# How Lazygit Implements Its Undo/Redo System Using the Git Reflog

> Discover how Lazygit uses the Git reflog to create an undo redo system Revert commits checkouts and rebases effortlessly by understanding its innovative approach.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: internals
- Published: 2026-03-02

---

**Lazygit implements undo and redo by walking the git reflog to identify user-initiated actions, using counter logic to skip previous undo markers, and executing inverse git commands to revert commits, checkouts, and rebases.**

The lazygit undo redo system transforms the git reflog from a passive audit log into an interactive history navigation tool. In the `jesseduffield/lazygit` repository, the implementation centers on the `UndoController` which parses reflog entries to distinguish between user actions and system-generated markers. This design allows developers to revert complex operations without manually calculating commit hashes or understanding git plumbing commands.

## Registering the Undo and Redo Keybindings

The user interface wires the `u` and `Ctrl-r` keys to reflog-based handlers inside [`pkg/gui/controllers/undo_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/undo_controller.go). The `GetKeybindings` method registers these actions with descriptive tooltips for the UI.

```go
func (self *UndoController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
    bindings := []*types.Binding{
        {
            Key:         opts.GetKey(opts.Config.Universal.Undo),   // default "u"
            Handler:     self.reflogUndo,
            Description: self.c.Tr.UndoReflog,
            Tooltip:     self.c.Tr.UndoTooltip,
        },
        {
            Key:         opts.GetKey(opts.Config.Universal.Redo),   // default "Ctrl-r"
            Handler:     self.reflogRedo,
            Description: self.c.Tr.RedoReflog,
            Tooltip:     self.c.Tr.RedoTooltip,
        },
    }
    return bindings
}

```

When triggered, `reflogUndo` and `reflogRedo` delegate to `parseReflogForActions`, the core parser that implements the lazygit reflog undo redo logic.

## Parsing the Git Reflog to Identify Reversible Actions

The heart of the system lives in `parseReflogForActions` (lines 200–246 of [`undo_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/undo_controller.go)). This function iterates over `self.c.Model().ReflogCommits`—an in-memory slice populated by the reflog loader—and classifies each entry using regex patterns on the reflog message.

### Detecting User Actions vs. System Markers

The parser distinguishes between genuine user operations and lazygit-generated entries by matching against specific patterns:

- **Checkout detection**: `^checkout: moving from ([\S]+) to ([\S]+)` creates a `CHECKOUT` action tracking the source and destination refs
- **Commit detection**: `^commit|^reset: moving to|^pull` creates a `COMMIT` action using the current hash and the previous entry's hash
- **Rebase detection**: Complex patterns identify `REBASE` operations by correlating `rebase (finish)` markers with subsequent `rebase (start)` entries
- **System markers**: `^\[lazygit undo\]` and `^\[lazygit redo\]` entries are injected by lazygit itself via the `GIT_REFLOG_ACTION` environment variable to track previous undo/redo cycles

### The Counter Logic That Tracks Undo State

A counter variable tracks how many undo operations must be skipped before finding the next reversible user action. When `reflogUndo` runs, the counter increments for every `[lazygit undo]` marker encountered; when it reaches zero, the parser has found the most recent user action that has not yet been undone.

```go
func (self *UndoController) parseReflogForActions(
    onUserAction func(counter int, action reflogAction) (bool, error),
) error {
    counter := 0
    reflogCommits := self.c.Model().ReflogCommits
    rebaseFinishCommitHash := ""
    var action *reflogAction

    for reflogCommitIdx, reflogCommit := range reflogCommits {
        action = nil
        prevCommitHash := ""
        if len(reflogCommits)-1 >= reflogCommitIdx+1 {
            prevCommitHash = reflogCommits[reflogCommitIdx+1].Hash()
        }

        if rebaseFinishCommitHash == "" {
            if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit undo\]`); ok {
                counter++
            } else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit redo\]`); ok {
                counter--
            } else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name,
                `^rebase (-i )?\(abort\)|^rebase (-i )?\(finish\)`); ok {
                rebaseFinishCommitHash = reflogCommit.Hash()
            } else if ok, match := utils.FindStringSubmatch(reflogCommit.Name,
                `^checkout: moving from ([\S]+) to ([\S]+)`); ok {
                action = &reflogAction{kind: CHECKOUT, from: match[1], to: match[2]}
            } else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name,
                `^commit|^reset: moving to|^pull`); ok {
                action = &reflogAction{kind: COMMIT, from: prevCommitHash, to: reflogCommit.Hash()}
            } else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name,
                `^rebase (-i )?\(start\)`); ok {
                action = &reflogAction{kind: CURRENT_REBASE, from: prevCommitHash}
            }
        } else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name,
            `^rebase (-i )?\(start\)`); ok {
            action = &reflogAction{kind: REBASE, from: prevCommitHash, to: rebaseFinishCommitHash}
            rebaseFinishCommitHash = ""
        }

        if action != nil {
            if action.kind != CURRENT_REBASE && action.from == action.to {
                continue
            }
            ok, err := onUserAction(counter, *action)
            if ok {
                return err
            }
            counter--
        }
    }
    return nil
}

```

When the callback returns `true`, the iteration stops and the controller executes the inverse command.

## Loading Reflog Entries Efficiently

The `ReflogCommitLoader` in [`pkg/commands/git_commands/reflog_commit_loader.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/reflog_commit_loader.go) (lines 25–63) populates the model by executing `git log -g --format=+%H%x00%ct%x00%gs%x00%P`. The `-g` flag instructs git to traverse reflog entries rather than the commit graph.

```go
func (self *ReflogCommitLoader) GetReflogCommits(
    hashPool *utils.StringPool,
    lastReflogCommit *models.Commit,
    filterPath string,
    filterAuthor string,
) ([]*models.Commit, bool, error) {
    cmdArgs := NewGitCmd("log").
        Config("log.showSignature=false").
        Arg("-g").
        Arg("--format=+%H%x00%ct%x00%gs%x00%P").
        ArgIf(filterAuthor != "", "--author="+filterAuthor).
        ArgIf(filterPath != "", "--follow", "--name-status", "--", filterPath).
        ToArgv()

    cmdObj := self.cmd.New(cmdArgs).DontLog()

    onlyObtainedNewReflogCommits := false

    commits, err := loadCommits(cmdObj, filterPath, func(line string) (*models.Commit, bool) {
        commit, ok := self.parseLine(hashPool, line)
        if !ok {
            return nil, false
        }
        if lastReflogCommit != nil && self.sameReflogCommit(commit, lastReflogCommit) {
            onlyObtainedNewReflogCommits = true
            return nil, true
        }
        return commit, false
    })
    return commits, onlyObtainedNewReflogCommits, err
}

```

The loader implements incremental refresh optimization: if the most recently loaded commit matches `lastReflogCommit`, the parser stops early, making repeated undo/redo operations performant even in large repositories.

## Executing Undo and Redo Operations

When `parseReflogForActions` identifies a reversible action with a counter value of zero, the controller invokes `self.c.Helpers().Refs.ResetToRef` or equivalent helpers to execute the inverse operation:

- **Commit undo**: `git reset --soft <from_hash>` restores the repository to the state before the commit while preserving working directory changes
- **Checkout undo**: `git checkout <from_hash>` returns to the previous branch or commit
- **Rebase undo**: Complex reset operations revert the repository to the state before the rebase began

Crucially, lazygit sets `GIT_REFLOG_ACTION=[lazygit undo]` (or `[lazygit redo]`) in the environment when executing these commands. This injects the marker entries that the parser later uses to maintain the counter state, creating a robust lazygit reflog undo redo cycle that correctly handles multiple consecutive undos or redos.

## Summary

- **Reflog walking**: The system parses `git log -g` output to identify CHECKOUT, COMMIT, and REBASE actions via regex patterns in [`undo_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/undo_controller.go)
- **Counter logic**: A counter tracks how many previous undo/redo operations to skip, ensuring the next action reverses the correct user-initiated change
- **Inverse commands**: Undo executes `git reset --soft` or `git checkout` to the previous state; redo applies the inverse in the opposite direction
- **System markers**: Environment variables inject `[lazygit undo]` and `[lazygit redo]` entries into the reflog to distinguish system actions from user actions
- **Incremental loading**: The `ReflogCommitLoader` optimizes performance by stopping when it encounters previously cached entries

## Frequently Asked Questions

### How does lazygit distinguish between user actions and its own undo operations?

Lazygit injects special markers into the reflog by setting the `GIT_REFLOG_ACTION` environment variable to `[lazygit undo]` or `[lazygit redo]` when executing reverse commands. The `parseReflogForActions` function in [`undo_controller.go`](https://github.com/jesseduffield/lazygit/blob/main/undo_controller.go) searches for these specific strings using regex patterns and increments or decrements a counter accordingly, ensuring these system-generated entries are skipped when calculating the next reversible action.

### What git commands does lazygit use to perform undo and redo?

For commit operations, lazygit executes `git reset --soft <hash>` to move HEAD without discarding working directory changes. For checkout operations, it runs `git checkout <hash>` to return to the previous reference. For rebases, it performs complex reset operations to the pre-rebase state. Each command is invoked through the `Refs` helper with special environment variables that mark the reflog entry as an undo or redo action.

### Can lazygit undo a git rebase operation?

Yes, the lazygit undo redo system supports rebases through specialized detection logic in `parseReflogForActions`. The parser correlates `rebase (finish)` markers with subsequent `rebase (start)` entries to construct a `REBASE` action type. When undoing, lazygit resets the repository to the commit hash recorded before the rebase began, effectively reverting the entire rebase sequence including any squashes, rewords, or reordering that occurred during the interactive session.

### How does lazygit optimize reflog loading for large repositories?

The `ReflogCommitLoader` in [`reflog_commit_loader.go`](https://github.com/jesseduffield/lazygit/blob/main/reflog_commit_loader.go) implements incremental loading by comparing newly fetched entries against `lastReflogCommit`. If the parser encounters a commit hash and timestamp matching the cached state, it sets `onlyObtainedNewReflogCommits` to true and stops processing. This prevents re-parsing the entire reflog history on every undo/redo operation, ensuring the UI remains responsive even when the reflog contains thousands of entries.