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:
- Execute porcelain command to get structured output
- Parse worktree entries to extract path, main worktree status, and current worktree status
- Parallel fetch git directories using
git rev-parse --absolute-git-dirfor each worktree - Detect special states by checking for rebase or bisect files in the git directory
- 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:
- Sets a waiting status (
Switching) - Changes the process cwd to the worktree path using
os.Chdir - Verifies the directory contains a valid Git repository
- Records the directory for recent repos tracking
- Invokes the global
onNewRepocallback 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 (
nby default): TriggersWorktreeHelper.NewWorktree()to create a worktree - Select/Enter: Invokes
WorktreeHelper.Switch()to change to the selected worktree - Remove (
Ctrl-d): CallsWorktreeHelper.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
Worktreemodel inpkg/commands/models/worktree.goencapsulates all worktree metadata including path, branch, git directory, and status flags. - Loading Mechanism:
WorktreeLoader.GetWorktreesinpkg/commands/git_commands/worktree_loader.goparses porcelain output and enriches entries with git directory paths and rebase/bisect status. - Command Wrappers:
WorktreeCommandsinpkg/commands/git_commands/worktree.goprovides thin, testable wrappers aroundgit worktree add,remove, anddetachoperations. - UI Orchestration:
WorktreeHelperinpkg/gui/controllers/helpers/worktree_helper.gomanages prompts, validation, and the complex logic for switching worktrees viaReposHelper.DispatchSwitchTo. - Controller Layer:
WorktreesControllerandWorktreeOptionsControllerbind 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →