How LazyGit Handles Git Worktree Creation and Management: A Complete Guide

LazyGit implements git worktree creation and management through a layered architecture that separates data models, git command wrappers, loading logic, and UI controllers, enabling users to create, switch, and delete worktrees without leaving the terminal.

LazyGit, the popular terminal UI for Git operations, provides comprehensive support for git worktree creation and management. Understanding how LazyGit handles worktrees reveals a well-architected system that bridges low-level git commands with an intuitive interface. This guide examines the source code to explain how LazyGit discovers, creates, switches, and removes worktrees.

LazyGit Git Worktree Creation and Management Architecture

The implementation follows a strict separation of concerns across five distinct layers. Each layer handles a specific aspect of the worktree lifecycle, from raw data representation to user interaction.

Layer Responsibility Key Types / Functions
Model Simple data struct that mirrors git worktree list output. models.Worktree
Git Commands Direct wrappers around git worktree CLI. WorktreeCommands.New, WorktreeCommands.Delete, WorktreeCommands.Detach
Git Loader Parses git worktree list --porcelain, enriches each entry (git dir, branch name, rebasing/bisect status). WorktreeLoader.GetWorktrees
Helper UI‑level orchestration (prompting, validation, dispatching to the repo helper). WorktreeHelper.NewWorktree, WorktreeHelper.Switch, WorktreeHelper.Remove, WorktreeHelper.Detach
Controller Binds keyboard shortcuts to helper actions and renders worktree details. WorktreesController, WorktreeOptionsController

The flow moves through these layers sequentially: Load → Display → Create → Switch → Delete/Detach.

The Worktree Data Model

At the foundation of lazygit git worktree creation and management lies the Worktree struct defined in pkg/commands/models/worktree.go. This model captures all metadata necessary to represent a worktree in the UI.

type Worktree struct {
    IsMain         bool   // true for the main repository worktree
    IsCurrent      bool   // true if this worktree is the one the user is currently in
    Path           string // absolute path to the worktree's working tree
    IsPathMissing  bool   // true if the directory no longer exists on disk
    GitDir         string // absolute path to the .git directory for the worktree
    Branch         string // name of the checked‑out branch (or empty for detached)
    Name           string // human‑readable unique name, derived from the path
}

Source: [pkg/commands/models/worktree.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/worktree.go)

This struct enables the UI to display worktree status, detect missing directories, and determine which worktree represents the user's current working directory.

Loading and Discovering Worktrees

The WorktreeLoader in pkg/commands/git_commands/worktree_loader.go handles the discovery of existing worktrees. It executes git worktree list --porcelain and transforms the output into a slice of models.Worktree instances.

The loading process follows these steps:

  1. Execute porcelain command to get structured output
  2. Parse worktree entries to extract path, main worktree status, and current worktree status
  3. Parallel fetch git directories using git rev-parse --absolute-git-dir for each worktree
  4. Detect special states by checking for rebase or bisect files in the git directory
  5. Generate unique display names based on paths to prevent UI collisions
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
...
if strings.HasPrefix(line, "worktree ") { // start of a new entry
    // fill IsMain, IsCurrent, IsPathMissing
}
...
wg.Add(len(worktrees))
for _, wt := range worktrees { go fetchGitDir(wt) }
...
// Resolve unique display names
names := getUniqueNamesFromPaths(...)
for i, wt := range worktrees { wt.Name = names[i] }

Source: [pkg/commands/git_commands/worktree_loader.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/git_commands/worktree_loader.go)

Creating New Worktrees

Creating a worktree in LazyGit involves the WorktreeHelper orchestrating user prompts before invoking WorktreeCommands.New. This process supports both branch-based and detached worktree creation.

The Command Wrapper

The low-level creation logic resides in pkg/commands/git_commands/worktree.go:

func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
    // `git worktree add [--detach] [-b <branch>] <path> <base>`
    cmdArgs := NewGitCmd("worktree").Arg("add").
        ArgIf(opts.Detach, "--detach").
        ArgIf(opts.Branch != "", "-b", opts.Branch).
        Arg(opts.Path, opts.Base)
    return self.cmd.New(cmdArgs.ToArgv()).Run()
}

Source: [pkg/commands/git_commands/worktree.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/git_commands/worktree.go)

The UI Orchestration

The WorktreeHelper.NewWorktree method in pkg/gui/controllers/helpers/worktree_helper.go handles the interactive flow:

func (self *WorktreeHelper) NewWorktree() error {
    // Prompt for the base ref (branch/commit) and whether to detach
    f := func(detached bool) {
        self.c.Prompt(types.PromptOpts{
            Title: self.c.Tr.NewWorktreeBase,
            InitialContent: currentBranchName,
            FindSuggestionsFunc: self.suggestionsHelper.GetRefsSuggestionsFunc(),
            HandleConfirm: func(base string) error {
                return self.NewWorktreeCheckout(base, true, detached, context.WORKTREES_CONTEXT_KEY)
            },
        })
    }
    // Show a menu offering "Create from branch" vs "Create detached"
    return self.c.Menu(types.CreateMenuOptions{
        Title: self.c.Tr.WorktreeTitle,
        Items: []*types.MenuItem{
            {LabelColumns: []string{self.c.Tr.CreateWorktreeFrom}, OnPress: func() error { f(false); return nil }},
            {LabelColumns: []string{self.c.Tr.CreateWorktreeFromDetached}, OnPress: func() error { f(true); return nil }},
        },
    })
}

Source: [pkg/gui/controllers/helpers/worktree_helper.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/helpers/worktree_helper.go)

This implementation allows users to create worktrees from existing branches or in detached HEAD state, with automatic path suggestions and validation.

Switching Between Worktrees

Switching to a different worktree involves changing the process working directory and reinitializing the UI context. The WorktreeHelper.Switch method delegates the heavy lifting to ReposHelper.DispatchSwitchTo.

func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error {
    if worktree.IsCurrent { return errors.New(self.c.Tr.AlreadyInWorktree) }
    self.c.LogAction(self.c.Tr.SwitchToWorktree)
    return self.reposHelper.DispatchSwitchTo(worktree.Path, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
}

Source: [pkg/gui/controllers/helpers/worktree_helper.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/helpers/worktree_helper.go)

The ReposHelper.DispatchSwitchTo function in pkg/gui/controllers/helpers/repos_helper.go performs the following:

  1. Sets a waiting status (Switching)
  2. Changes the process cwd to the worktree path using os.Chdir
  3. Verifies the directory contains a valid Git repository
  4. Records the directory for recent repos tracking
  5. Invokes the global onNewRepo callback to reinitialize the UI with the new repository context

This approach ensures that when you switch worktrees in LazyGit, the entire UI context updates to reflect the new working directory, branches, and state.

Deleting and Detaching Worktrees

LazyGit provides two distinct removal operations: standard deletion and detaching. The WorktreeHelper handles both with appropriate safety checks.

Removing a Worktree

The WorktreeHelper.Remove method implements a confirmation flow with automatic force-retry logic:

func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
    // Confirmation dialog → git worktree remove (optionally –force)
    self.c.Confirm(types.ConfirmOpts{
        Title:  self.c.Tr.RemoveWorktreeTitle,
        Prompt: utils.ResolvePlaceholderString(...),
        HandleConfirm: func() error {
            return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
                if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
                    // If not forced and error suggests a need for –force, retry with force.
                    if !force && shouldForce(err) { return self.Remove(worktree, true) }
                    return err
                }
                self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
                return nil
            })
        },
    })
    return nil
}

Source: [pkg/gui/controllers/helpers/worktree_helper.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/helpers/worktree_helper.go)

This implementation detects when a worktree contains uncommitted changes or submodules that require force removal, automatically prompting the user to retry with the -f flag.

Detaching a Worktree

Detaching removes the worktree entry from git's management without deleting the working directory files:

func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
    return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
        self.c.LogAction(self.c.Tr.RemovingWorktree)
        return self.c.Git().Worktree.Detach(worktree.Path)
    })
}

Source: [pkg/gui/controllers/helpers/worktree_helper.go](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/controllers/helpers/worktree_helper.go)

Controller Layer and UI Integration

The controller layer binds keyboard shortcuts to the helper methods and renders the worktree list. The WorktreesController in pkg/gui/controllers/worktrees_controller.go manages the primary worktree view.

Key bindings include:

  • New (n by default): Triggers WorktreeHelper.NewWorktree() to create a worktree
  • Select/Enter: Invokes WorktreeHelper.Switch() to change to the selected worktree
  • Remove (Ctrl-d): Calls WorktreeHelper.Remove() for deletion
  • Open: Launches the worktree path in an external editor

The controller renders the list using a tabwriter to display columns for name, branch, path, and status flags (main, missing).

Additionally, the WorktreeOptionsController in pkg/gui/controllers/worktree_options_controller.go provides context-menu access to create worktrees from the currently selected branch or ref, streamlining the creation workflow.

Summary

LazyGit's approach to git worktree creation and management follows a clean architectural separation that ensures reliability and responsiveness:

  • Data Layer: The Worktree model in pkg/commands/models/worktree.go encapsulates all worktree metadata including path, branch, git directory, and status flags.
  • Loading Mechanism: WorktreeLoader.GetWorktrees in pkg/commands/git_commands/worktree_loader.go parses porcelain output and enriches entries with git directory paths and rebase/bisect status.
  • Command Wrappers: WorktreeCommands in pkg/commands/git_commands/worktree.go provides thin, testable wrappers around git worktree add, remove, and detach operations.
  • UI Orchestration: WorktreeHelper in pkg/gui/controllers/helpers/worktree_helper.go manages prompts, validation, and the complex logic for switching worktrees via ReposHelper.DispatchSwitchTo.
  • Controller Layer: WorktreesController and WorktreeOptionsController bind keyboard shortcuts and render the worktree list with status indicators.

This pipeline enables users to seamlessly create worktrees from branches or detached states, switch between them with automatic UI context updates, and safely remove them with intelligent force-detection.

Frequently Asked Questions

How does LazyGit discover existing git worktrees?

LazyGit discovers worktrees by executing git worktree list --porcelain through the WorktreeLoader.GetWorktrees function in pkg/commands/git_commands/worktree_loader.go. The loader parses the porcelain output to extract paths, then parallel-fetches each worktree's git directory using git rev-parse --absolute-git-dir. It also detects special states like rebasing or bisecting by examining files within each worktree's git directory, finally generating unique display names based on the paths to prevent UI collisions.

What happens when I switch to a different worktree in LazyGit?

When you switch worktrees, WorktreeHelper.Switch in pkg/gui/controllers/helpers/worktree_helper.go validates that you're not already in the target worktree, then delegates to ReposHelper.DispatchSwitchTo. This helper changes the process's current working directory to the worktree path using os.Chdir, verifies the directory contains a valid Git repository, records the path for recent repository tracking, and invokes the global onNewRepo callback. This callback reinitializes the entire UI context, refreshing views for branches, files, and commits to reflect the new worktree's state.

Can LazyGit create detached worktrees?

Yes, LazyGit supports creating detached worktrees through the WorktreeHelper.NewWorktree function. When initiating creation, the UI presents a menu offering "Create worktree from branch" or "Create detached worktree". Selecting the detached option sets the Detach flag to true in NewWorktreeOpts, which the WorktreeCommands.New method in pkg/commands/git_commands/worktree.go translates into the --detach flag when executing git worktree add. This allows you to create a worktree pointing to a specific commit or tag without creating a local branch.

How does LazyGit handle errors when deleting worktrees?

LazyGit implements intelligent error handling for worktree deletion in WorktreeHelper.Remove within pkg/gui/controllers/helpers/worktree_helper.go. When WorktreeCommands.Delete returns an error, the helper checks if the error indicates that the worktree contains uncommitted changes or submodules that require force removal. If the initial attempt was made without the force flag, the function recursively calls itself with force=true, which translates to the -f flag in git worktree remove. The UI displays appropriate confirmation dialogs before deletion and refreshes the worktree, branch, and file views upon successful completion.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →