Improve startup time (#5934)

This PR has two separate improvements for the startup time, in
particular for the time until the Files panel shows the modified files:

- avoid walking the `.git/workspaces` dir recursively, looking for the
gitdir files of linked worktrees. This code was not used to populate the
worktrees panel, but only for the decision in the Files panel whether to
show an entry with the worktree icon; it was doing unnecessary work,
because walking the `.git/workspaces` dir recursively is pointless (git
stores the worktree gitdir files only at the top level, so a flat read
of that directory would have been enough), and can take significant time
in large repos, especially when they have many submodules. Instead of
fixing that code, remove it entirely and rearrange the refresh code so
that we can use the regular worktree model for this Files panel
decision.
- at startup we were doing two full refreshes at the same time: the
regular one that we always do after loading a repo for the first time,
and then also a focus-in refresh. I didn't realize that a terminal will
send us a focus-in event right at the moment we request these events.
Nothing bad happens from doing those two refreshes at the same time, but
it slows things down a bit when we have two "git status" calls running
concurrently.

Both of these together reduce the time it takes for the Files panel to
show its files at startup (in my regular work repo with three worktrees
and ~30 submodules) from 860ms to 440ms, so almost a factor of two. If
you want to measure this in your own repo, here's a small throwaway
patch that you can use for that:

```diff
diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go
index 40ce86eae..fbb482f39 100644
--- a/pkg/gui/controllers/helpers/refresh_helper.go
+++ b/pkg/gui/controllers/helpers/refresh_helper.go
@@ -26,6 +26,11 @@ import (
 	"github.com/sasha-s/go-deadlock"
 )
 
+var (
+	applicationStartTime     = time.Now()
+	applicationStartTimeOnce sync.Once
+)
+
 type RefreshHelper struct {
 	c                    *HelperCommon
 	refsHelper           *RefsHelper
@@ -1213,6 +1218,10 @@ func (self *RefreshHelper) refreshFilesAndSubmodules(captured capturedFilesState
 	self.refreshView(self.c.Contexts().Submodules, env)
 	self.refreshView(self.c.Contexts().Files, env)
 
+	applicationStartTimeOnce.Do(func() {
+		self.c.Log.Infof("Time until first files refresh: %s", time.Since(applicationStartTime))
+	})
+
 	return nil
 }
 
```

To use it, run `./lazygit -l | grep "first files refresh"` in one
terminal, and `lazygit -d` in the one that you want to test. I'm curious
about your before/after measurements, feel free to post them below in
the comments.
This commit is contained in:
Stefan Haller 2026-08-15 12:02:33 +02:00 committed by GitHub
commit ae1007612b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 233 additions and 127 deletions

View file

@ -2,7 +2,6 @@ package git_commands
import (
"fmt"
"path/filepath"
"strconv"
"strings"
@ -91,26 +90,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
self.setConflictMarkerSizes(files)
// Go through the files to see if any of these files are actually worktrees
// so that we can render them correctly
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
for _, file := range files {
for _, worktreePath := range worktreePaths {
absFilePath, err := filepath.Abs(file.Path)
if err != nil {
self.Log.Error(err)
continue
}
if absFilePath == worktreePath {
file.IsWorktree = true
// `git status` renders this worktree as a folder with a trailing slash but we'll represent it as a singular worktree
// If we include the slash, it will be rendered as a folder with a null file inside.
file.Path = strings.TrimSuffix(file.Path, "/")
break
}
}
}
return files
}

View file

@ -1,7 +1,6 @@
package git_commands
import (
ioFs "io/fs"
"os"
"path/filepath"
"strings"
@ -10,7 +9,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spf13/afero"
)
type RepoPaths struct {
@ -302,41 +300,3 @@ func runGitRevParse(gitCmd *oscommands.CmdObj) (string, error) {
}
return strings.TrimSpace(res), nil
}
// Returns the paths of linked worktrees
func linkedWortkreePaths(fs afero.Fs, repoGitDirPath string) []string {
result := []string{}
// For each directory in this path we're going to cat the `gitdir` file and append its contents to our result
// That file points us to the `.git` file in the worktree.
worktreeGitDirsPath := filepath.Join(repoGitDirPath, "worktrees")
// ensure the directory exists
_, err := fs.Stat(worktreeGitDirsPath)
if err != nil {
return result
}
_ = afero.Walk(fs, worktreeGitDirsPath, func(currPath string, info ioFs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
return nil
}
gitDirPath := filepath.Join(currPath, "gitdir")
gitDirBytes, err := afero.ReadFile(fs, gitDirPath)
if err != nil {
// ignoring error
return nil
}
trimmedGitDir := strings.TrimSpace(string(gitDirBytes))
// removing the .git part
worktreeDir := filepath.Dir(trimmedGitDir)
result = append(result, worktreeDir)
return nil
})
return result
}

View file

@ -222,6 +222,11 @@ type Gui struct {
// worker goroutines, so it's atomic.
uiThreadID atomic.Int64
// focused says whether the terminal we're running in has focus, as far as
// its focus reports tell us (see IsFocused). Written by the event loop,
// readable from anywhere, so it's atomic.
focused atomic.Bool
// blockInputCount, when greater than zero, withholds keyboard input from
// the handlers: key events are buffered into bufferedKeyEvents and replayed
// once the count drops back to zero, while mouse clicks and hover are
@ -306,6 +311,12 @@ func NewGui(opts NewGuiOpts) (*Gui, error) {
// runs during startup, before we reach MainLoop.
g.uiThreadID.Store(goid.Get())
// Assume we start out focused: a terminal that supports focus reports sends
// one for the state it is already in when we turn reporting on in MainLoop,
// and passing that on as a change would have the app react to a change that
// never happened.
g.focused.Store(true)
return g, nil
}
@ -2009,7 +2020,21 @@ func (g *Gui) execKeybinding(v *View, kb *keybinding) error {
return nil
}
// IsFocused reports whether the terminal we're running in has focus. Terminals
// that don't report focus at all leave this true for good.
func (g *Gui) IsFocused() bool {
return g.focused.Load()
}
func (g *Gui) onFocus(ev *GocuiEvent) error {
// Terminals report their focus state when we turn focus reporting on, and
// some report it again when their window is activated, so only pass on the
// reports that actually change it.
if ev.Focused == g.focused.Load() {
return nil
}
g.focused.Store(ev.Focused)
if g.focusHandler != nil {
return g.focusHandler(ev.Focused)
}

View file

@ -273,6 +273,10 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
// - merge conflicts are part of what the files refresh produces
// - pull requests are fetched for the tracking branches against the
// remotes, so refresh both alongside to fetch against fresh data
// - commits and branches always go together: changing commits changes
// the branches' upstream/downstream counts, and changing branches
// (e.g. checking one out) changes the commits we show. This one comes
// last, so that it also covers the branches the rules above add.
if scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
scopeSet.Add(types.COMMITS, types.BRANCHES)
}
@ -285,6 +289,9 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
if scopeSet.Includes(types.PULL_REQUESTS) {
scopeSet.Add(types.BRANCHES, types.REMOTES)
}
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) {
scopeSet.Add(types.COMMITS, types.BRANCHES)
}
// Capture the refs snapshot now, before we start reading git's state
// below, rather than after. This is important to guard against the race
@ -311,6 +318,23 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
})
}
// The branches view shows worktrees against branches, so a branches render
// that happens before the refreshed worktrees have landed in the model shows
// stale ones, and rendering again once they land makes the view flicker.
// Refresh the worktrees first, then, and let the branches refresh wait for
// them: waitForWorktrees returns once the worktrees model write is queued,
// so the branches write that follows is queued behind it and the view
// renders once, with both.
worktreesWg := sync.WaitGroup{}
waitForWorktrees := func() { worktreesWg.Wait() }
if scopeSet.Includes(types.WORKTREES) {
worktreesWg.Add(1)
refresh("worktrees", func() {
defer worktreesWg.Done()
self.refreshWorktrees(env, scopeSet.Includes(types.BRANCHES))
})
}
branchesAndRemotesWg := sync.WaitGroup{}
// The pull-request fetch (below) needs the just-loaded branches and
// remotes. Their model writes are bounced onto the UI thread, so the
@ -320,52 +344,23 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
// branchesAndRemotesWg gives the fetch the happens-before to read them.
var loadedBranches []*models.Branch
var loadedRemotes []*models.Remote
includeWorktreesWithBranches := false
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) {
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should also change commits
// e.g. in the case of switching branches.
// Capture the commits, reflog and branches refresh inputs (model,
// contexts, modes) on the UI thread, before the git work is dispatched
// to a worker, so the workers compute from an immutable snapshot
// instead of reading state the UI thread concurrently mutates.
if scopeSet.Includes(types.COMMITS) {
// Capture the refresh's inputs (model, contexts, modes) on the UI
// thread, before the git work is dispatched to a worker, so the worker
// computes from an immutable snapshot instead of reading state the UI
// thread concurrently mutates. Every scope below does the same.
var capturedCommits capturedCommitState
var capturedReflog capturedReflogState
var capturedBranches capturedBranchState
if !self.captureOnUIThread(calledFromWorker, env.background, func() {
capturedCommits = self.captureCommitsState()
capturedReflog = self.captureReflogState()
capturedBranches = self.captureBranchState()
}) {
return
}
refresh("commits and commit files", func() {
self.refreshCommitsAndCommitFiles(capturedCommits, options.CommitSelection, env)
})
includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES)
if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" {
branchesAndRemotesWg.Add(1)
refresh("reflog and branches", func() {
loadedBranches = self.refreshReflogAndBranches(capturedReflog, capturedBranches, includeWorktreesWithBranches, options.BranchSelection, options.SelectTopReflogCommit, env)
branchesAndRemotesWg.Done()
})
} else {
branchesAndRemotesWg.Add(1)
refresh("branches", func() {
// Not a recency sort, so branches doesn't depend on the reflog
// being fresh; it runs concurrently with the reflog refresh
// below and uses the reflog we captured up front, as it always has.
loadedBranches = self.refreshBranches(capturedBranches, includeWorktreesWithBranches, options.BranchSelection, true, capturedReflog.reflogCommits, env)
branchesAndRemotesWg.Done()
})
refresh("reflog", func() {
_, _ = self.refreshReflogCommits(capturedReflog, env, options.SelectTopReflogCommit)
})
}
} else if scopeSet.Includes(types.REBASE_COMMITS) {
// the above block handles rebase commits so we only need to call this one
// if we've asked specifically for rebase commits and not those other things
// the commits refresh above loads the rebase commits as well, so we only
// need this one when the rebase commits are all that was asked for
var rebaseHashPool *utils.StringPool
var rebaseCommits []*models.Commit
if !self.captureOnUIThread(calledFromWorker, env.background, func() {
@ -376,6 +371,39 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
refresh("rebase commits", func() { _ = self.refreshRebaseCommits(rebaseHashPool, rebaseCommits, env) })
}
if scopeSet.Includes(types.BRANCHES) {
// The reflog is refreshed here rather than in a scope of its own,
// because sorting the branches by recency needs it to be loaded first.
var capturedReflog capturedReflogState
var capturedBranches capturedBranchState
if !self.captureOnUIThread(calledFromWorker, env.background, func() {
capturedReflog = self.captureReflogState()
capturedBranches = self.captureBranchState()
}) {
return
}
if self.c.UserConfig().Git.LocalBranchSortOrder == "recency" {
branchesAndRemotesWg.Add(1)
refresh("reflog and branches", func() {
loadedBranches = self.refreshReflogAndBranches(capturedReflog, capturedBranches, waitForWorktrees, options.BranchSelection, options.SelectTopReflogCommit, env)
branchesAndRemotesWg.Done()
})
} else {
branchesAndRemotesWg.Add(1)
refresh("branches", func() {
// Not a recency sort, so branches doesn't depend on the reflog
// being fresh; it runs concurrently with the reflog refresh
// below and uses the reflog we captured up front, as it always has.
loadedBranches = self.refreshBranches(capturedBranches, waitForWorktrees, options.BranchSelection, true, capturedReflog.reflogCommits, env)
branchesAndRemotesWg.Done()
})
refresh("reflog", func() {
_, _ = self.refreshReflogCommits(capturedReflog, env, options.SelectTopReflogCommit)
})
}
}
if scopeSet.Includes(types.SUB_COMMITS) {
var capturedSubCommits capturedSubCommitState
if !self.captureOnUIThread(calledFromWorker, env.background, func() {
@ -468,10 +496,6 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
})
}
if scopeSet.Includes(types.WORKTREES) && !includeWorktreesWithBranches {
refresh("worktrees", func() { self.refreshWorktrees(env) })
}
if scopeSet.Includes(types.STAGING) {
refresh("staging", func() {
fileWg.Wait()
@ -698,17 +722,19 @@ func (self *RefreshHelper) captureBranchState() capturedBranchState {
}
}
func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflogState, capturedBranches capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, env refreshEnv) []*models.Branch {
func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflogState, capturedBranches capturedBranchState, waitForWorktrees func(), branchSelection types.BranchSelectionBehavior, selectTopReflogCommit bool, env refreshEnv) []*models.Branch {
switch self.c.State().GetRepoState().GetStartupStage() {
case types.INITIAL:
// Return the immediate (non-recency) load's branches; the recency-sorted
// reload below runs on its own worker after we return. Both hold the same
// set of branches, which is all the caller (the PR fetch) needs.
branches := self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, false, capturedReflog.reflogCommits, env)
branches := self.refreshBranches(capturedBranches, waitForWorktrees, branchSelection, false, capturedReflog.reflogCommits, env)
self.onWorker(env.background, func(_ gocui.Task) error {
reflogCommits, _ := self.refreshReflogCommits(capturedReflog, env, false)
self.refreshBranches(capturedBranches, false, types.SelectCheckedOutBranch, true, reflogCommits, env)
// The load above already waited for the worktrees, so this one has
// nothing left to wait for.
self.refreshBranches(capturedBranches, func() {}, types.SelectCheckedOutBranch, true, reflogCommits, env)
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
return nil
})
@ -717,7 +743,7 @@ func (self *RefreshHelper) refreshReflogAndBranches(capturedReflog capturedReflo
case types.COMPLETE:
reflogCommits, _ := self.refreshReflogCommits(capturedReflog, env, selectTopReflogCommit)
return self.refreshBranches(capturedBranches, refreshWorktrees, branchSelection, true, reflogCommits, env)
return self.refreshBranches(capturedBranches, waitForWorktrees, branchSelection, true, reflogCommits, env)
}
return nil
@ -1082,7 +1108,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs(env refreshEnv) ([]*mode
// self.refreshStatus is called at the end of this because that's when we can
// be sure there is a State.Model.Branches array to pick the current branch from
func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refreshWorktrees bool, branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, env refreshEnv) []*models.Branch {
func (self *RefreshHelper) refreshBranches(captured capturedBranchState, waitForWorktrees func(), branchSelection types.BranchSelectionBehavior, loadBehindCounts bool, reflogCommits []*models.Commit, env refreshEnv) []*models.Branch {
loadSeq := self.branchLoadSeq.Add(1)
branches, err := env.git.Loaders.BranchLoader.Load(
@ -1116,10 +1142,9 @@ func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refresh
self.c.Log.Error(err)
}
var worktrees []*models.Worktree
if refreshWorktrees {
worktrees = self.loadWorktrees(env)
}
// Render only once the refreshed worktrees are in the model; the branches
// view shows them against the branches (see performRefresh).
waitForWorktrees()
self.onUIThreadUnlessRepoChanged(env, func() {
// Drop this write if a branch load that started later has already applied
@ -1142,11 +1167,6 @@ func (self *RefreshHelper) refreshBranches(captured capturedBranchState, refresh
// the branches we just wrote, on the UI thread.
self.rebuildPullRequestsMap()
if refreshWorktrees {
self.c.Model().Worktrees = worktrees
self.refreshView(self.c.Contexts().Worktrees, env)
}
// Setting the selection here, in the same bounce that writes the list,
// keeps it on the UI thread and keeps the list and selection updating in
// the same frame.
@ -1399,12 +1419,45 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re
self.c.Model().Submodules = submoduleConfigs
self.c.Model().Files = files
markWorktreeFiles(files, self.c.Model().Worktrees, env.git.RepoPaths.WorktreePath())
fileTreeViewModel.SetTree()
})
return nil
}
// markWorktreeFiles marks the files that are linked worktrees of this repo, so
// that the files view can render them as such. `git status` reports a worktree
// as an untracked directory, i.e. with a trailing slash, which we take off:
// keeping it would build a directory node with a nameless file inside it.
//
// It must run on the UI thread, as it works on the model. Both models it needs
// are written by refreshes of their own, so it is called after either of them
// lands; it reports whether it changed anything.
func markWorktreeFiles(files []*models.File, worktrees []*models.Worktree, worktreePath string) bool {
changed := false
for _, file := range files {
absPath := filepath.Join(worktreePath, file.Path)
isWorktree := lo.SomeBy(worktrees, func(worktree *models.Worktree) bool {
return worktree.Path == absPath
})
if isWorktree != file.IsWorktree {
file.IsWorktree = isWorktree
changed = true
}
if isWorktree {
if trimmed := strings.TrimSuffix(file.Path, "/"); trimmed != file.Path {
file.Path = trimmed
changed = true
}
}
}
return changed
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying
@ -1506,16 +1559,27 @@ func (self *RefreshHelper) loadWorktrees(env refreshEnv) []*models.Worktree {
return worktrees
}
func (self *RefreshHelper) refreshWorktrees(env refreshEnv) {
func (self *RefreshHelper) refreshWorktrees(env refreshEnv, branchesAreRefreshing bool) {
worktrees := self.loadWorktrees(env)
self.onUIThreadUnlessRepoChanged(env, func() {
self.c.Model().Worktrees = worktrees
// A worktree inside our working tree is one of the files, so the files
// view has to be told about the ones we just loaded (see
// markWorktreeFiles). Rebuild the tree because a file's path can change.
if markWorktreeFiles(self.c.Model().Files, worktrees, env.git.RepoPaths.WorktreePath()) {
self.c.Contexts().Files.FileTreeViewModel.SetTree()
self.refreshView(self.c.Contexts().Files, env)
}
})
// need to refresh branches because the branches view shows worktrees against
// branches
self.refreshView(self.c.Contexts().Branches, env)
// The branches view shows worktrees against branches, so it needs to be
// rendered again as well. When the branches are being refreshed too, they
// render after waiting for the write above, so leave it to them.
if !branchesAreRefreshing {
self.refreshView(self.c.Contexts().Branches, env)
}
self.refreshView(self.c.Contexts().Worktrees, env)
}

View file

@ -1,6 +1,7 @@
package helpers
import (
"path/filepath"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
@ -243,6 +244,46 @@ func TestGetAuthenticatedGithubRemotes(t *testing.T) {
}, callsByHost)
}
func TestMarkWorktreeFiles(t *testing.T) {
worktreePath := filepath.Join("/", "path", "to", "repo")
worktrees := []*models.Worktree{
{Path: worktreePath},
{Path: filepath.Join(worktreePath, "worktree1")},
{Path: filepath.Join(worktreePath, "dir", "worktree2")},
{Path: filepath.Join("/", "path", "to", "worktree3")},
}
t.Run("marks the files that are worktrees, and takes their slash off", func(t *testing.T) {
files := []*models.File{
{Path: "file"},
{Path: "worktree1/"},
{Path: "dir/worktree2/"},
{Path: "dir/"},
}
assert.True(t, markWorktreeFiles(files, worktrees, worktreePath))
assert.Equal(t, []*models.File{
{Path: "file"},
{Path: "worktree1", IsWorktree: true},
{Path: "dir/worktree2", IsWorktree: true},
{Path: "dir/"},
}, files)
})
t.Run("reports no change when there is nothing to mark", func(t *testing.T) {
files := []*models.File{{Path: "file"}, {Path: "dir/"}}
assert.False(t, markWorktreeFiles(files, worktrees, worktreePath))
})
t.Run("unmarks a file whose worktree is gone", func(t *testing.T) {
files := []*models.File{{Path: "worktree1", IsWorktree: true}}
assert.True(t, markWorktreeFiles(files, nil, worktreePath))
assert.Equal(t, []*models.File{{Path: "worktree1"}}, files)
})
}
func makeGithubRemoteInfoList(names ...string) []githubRemoteInfo {
return lo.Map(names, func(name string, _ int) githubRemoteInfo {
return makeGithubRemoteInfo(name, name)

View file

@ -101,14 +101,25 @@ func (self *GuiDriver) replayMouseEventWithoutWaiting(x, y int, buttons tcell.Bu
))
}
// FocusIn simulates the terminal window regaining focus, which is how lazygit
// learns to reload changed config files. Tests use it to exercise the live
// config-reload path.
func (self *GuiDriver) FocusIn() {
// replayFocusIn takes the focus away before handing it back, because that's the
// only way a terminal can report regaining it, and lazygit only reacts to focus
// reports that change the focus (see gocui.Gui.IsFocused).
func (self *GuiDriver) replayFocusIn() {
self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper(
tcell.NewEventFocus(false),
0,
))
self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper(
tcell.NewEventFocus(true),
0,
))
}
// FocusIn simulates the terminal window regaining focus, which is how lazygit
// learns to reload changed config files. Tests use it to exercise the live
// config-reload path.
func (self *GuiDriver) FocusIn() {
self.replayFocusIn()
self.waitTillIdle()
}
@ -116,10 +127,7 @@ func (self *GuiDriver) FocusIn() {
func (self *GuiDriver) FocusInAndClick(x, y int) {
self.CheckAllToastsAcknowledged()
self.gui.g.ReplayFocusEvent(gocui.NewTcellFocusEventWrapper(
tcell.NewEventFocus(true),
0,
))
self.replayFocusIn()
self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper(
tcell.NewEventMouse(x, y, tcell.ButtonPrimary, 0),
0,

View file

@ -557,4 +557,5 @@ var tests = []*components.IntegrationTest{
worktree.SeparateWorkTreeConfig,
worktree.SymlinkIntoRepoSubdir,
worktree.WorktreeInRepo,
worktree.WorktreeInsideRepo,
}

View file

@ -0,0 +1,28 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var WorktreeInsideRepo = NewIntegrationTest(NewIntegrationTestArgs{
Description: "A worktree that lives inside the repo's working tree is shown as a single item in the files panel",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Gui.NerdFontsVersion = "3"
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.AddWorktree("mybranch", "nested-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Equals("?? 󰌹 nested-worktree").IsSelected(),
)
},
})