mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
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:
commit
ae1007612b
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -557,4 +557,5 @@ var tests = []*components.IntegrationTest{
|
|||
worktree.SeparateWorkTreeConfig,
|
||||
worktree.SymlinkIntoRepoSubdir,
|
||||
worktree.WorktreeInRepo,
|
||||
worktree.WorktreeInsideRepo,
|
||||
}
|
||||
|
|
|
|||
28
pkg/integration/tests/worktree/worktree_inside_repo.go
Normal file
28
pkg/integration/tests/worktree/worktree_inside_repo.go
Normal 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(),
|
||||
)
|
||||
},
|
||||
})
|
||||
Loading…
Reference in a new issue