1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2026-05-22 10:15:43 +02:00

Avoid auto-forwarding main branches checked out in other worktrees (#5621)

After fetching, we auto-forward main branches that have fallen behind
their upstream, but skip any that are currently checked out in another
worktree — otherwise we'd update the ref behind that worktree's back,
leaving its working copy showing the inverse of what was just fetched.

The skip check had a hole though: when starting lazygit while no
worktrees have a main branch checked out, leaving it running in the
background, and then checking out a main branch in one of the other
worktrees outside of lazygit (e.g. in a lazygit instance in another
terminal, or using `git checkout` in the shell, or using some other git
client or IDE), then lazygit wouldn't notice the change, and the next
fetch would auto-forward main even though it is now checked out in a
worktree.

The fix is to include `WORKTREES` in the post-fetch refresh scope when
auto-forwarding is enabled. We gate on the config so users with
auto-forward disabled don't pay for an extra `git worktree list` plus
per-worktree rev-parse on every fetch tick.

A few small things picked up along the way landed as separate commits
first:

- Preserve the empty-slice fallback in `loadWorktrees` when `git
worktree list` fails — the fallback was being overwritten by the nil
return value on the next line.
- Add `PULL_REQUESTS` to the manual fetch refresh scope to match the
background fetch; looks like an oversight from when PR support was
added.
- Extract `BranchesHelper.PostFetchRefresh` so the two fetch paths can't
drift again.

Fixes #5020.
This commit is contained in:
Stefan Haller
2026-05-21 14:27:44 +02:00
committed by GitHub
7 changed files with 120 additions and 16 deletions
+41
View File
@@ -106,13 +106,54 @@ the buggy one, so the file compiles and the test passes against unfixed code.
In the fix commit, remove the comment markers and delete the `ACTUAL` line.
Don't explain the pattern in commit messages.
The fix commit must be _exactly_ "delete the markers and delete the `ACTUAL`
line" — no other edits. That means `EXPECTED` and `ACTUAL` have to be drop-in
replacements for each other at the same syntactic position. If you can't write
them that way (e.g. one is `.IsEmpty()` and the other is `.Lines(...)`),
restructure the surrounding code until you can — usually by putting the
comment block between two adjacent chained calls, so both forms are just the
next method in the chain:
```go
t.Views().Files().
Focus().
/* EXPECTED:
IsEmpty()
ACTUAL: */
Lines(
Equals("D file03.txt"),
)
```
If you find yourself reaching for a local variable so that both forms can be
expressed against the same receiver, the structure isn't right yet — go back
and fix it instead of papering over it with a binding.
Use this pattern only where it makes sense; don't apply it by default.
## Integration test conventions
Don't bind views to local variables. Always chain method calls directly from
`t.Views().<View>()`. Patterns like `filesView := t.Views().Files().Focus()`
followed by `filesView.Lines(...)` are not how tests in this repo are written;
keep the call site fluent.
## Use stretchr/testify for assertions
Prefer `assert.Equal` (and friends) over hand-rolled `if` checks. The failure
messages are more useful and the intent is clearer at a glance.
## Don't present "live with the bug" as an option
When you're investigating a defect and laying out fix options for the user,
"accept the race / leave it as-is / document it and move on" is not one of
them. A known race condition, data corruption, or correctness violation is a
bug that needs a real fix, not a tradeoff. Even if the failure rate is low,
even if the window is tiny, even if no current code path appears to hit it —
present actual fixes. If a real fix is genuinely out of reach (e.g. it
requires API changes you can't make), say so plainly; don't dress "no fix"
up as a viable option in a numbered list alongside real ones.
## Don't search outside the working tree
Never run `find` (or similar) from `/` or other paths outside the project. All
+1 -7
View File
@@ -155,13 +155,7 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.FetchBackground()
self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS}, Mode: types.SYNC})
if err == nil {
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
}
return err
return self.gui.helpers.BranchesHelper.PostFetchRefresh(err)
}
func (self *BackgroundRoutineMgr) triggerImmediateFetch() {
+1 -7
View File
@@ -1348,13 +1348,7 @@ func (self *FilesController) fetch() error {
return errors.New(self.c.Tr.PassUnameWrong)
}
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
if err == nil {
err = self.c.Helpers().BranchesHelper.AutoForwardBranches()
}
return err
return self.c.Helpers().BranchesHelper.PostFetchRefresh(err)
})
}
@@ -285,6 +285,21 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
return nil
}
func (self *BranchesHelper) PostFetchRefresh(fetchErr error) error {
scope := []types.RefreshableView{
types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS, types.PULL_REQUESTS,
}
// AutoForwardBranches needs a fresh worktree model to skip branches that are checked out elsewhere.
if self.c.UserConfig().Git.AutoForwardBranches != "none" {
scope = append(scope, types.WORKTREES)
}
self.c.Refresh(types.RefreshOptions{Scope: scope, Mode: types.SYNC})
if fetchErr != nil {
return fetchErr
}
return self.AutoForwardBranches()
}
func (self *BranchesHelper) AutoForwardBranches() error {
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
return nil
@@ -724,9 +724,9 @@ func (self *RefreshHelper) loadWorktrees() {
if err != nil {
self.c.Log.Error(err)
self.c.Model().Worktrees = []*models.Worktree{}
} else {
self.c.Model().Worktrees = worktrees
}
self.c.Model().Worktrees = worktrees
}
func (self *RefreshHelper) refreshWorktrees() {
@@ -0,0 +1,59 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FetchAndAutoForwardBranchesWorktreeAddedAfterStartup = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Auto-forward skips a main branch that was externally checked out in a linked worktree after lazygit started",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AutoForwardBranches = "onlyMainBranches"
config.GetUserConfig().Git.LocalBranchSortOrder = "alphabetical"
},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(3)
shell.NewBranch("feature")
shell.NewBranch("wt-branch")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("feature", "origin/feature")
shell.Checkout("master")
shell.HardReset("HEAD^")
shell.Checkout("feature")
shell.AddWorktreeCheckout("wt-branch", "../linked-worktree")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("feature").IsSelected(),
Contains("master ↓1").DoesNotContain("↑"),
Contains("wt-branch (worktree linked-worktree)"),
)
// Switch the linked worktree to master externally.
t.Shell().RunCommand([]string{"git", "-C", "../linked-worktree", "checkout", "master"})
t.Views().Files().
IsFocused().
Press(keys.Files.Fetch)
t.Views().Branches().
Lines(
Contains("feature").IsSelected(),
Contains("master (worktree linked-worktree) ↓1"),
Contains("wt-branch").DoesNotContain("worktree"),
)
t.Views().Worktrees().
Focus().
NavigateToLine(Contains("linked-worktree")).
PressPrimaryAction()
t.Views().Files().
Focus().
IsEmpty()
},
})
+1
View File
@@ -428,6 +428,7 @@ var tests = []*components.IntegrationTest{
sync.FetchAndAutoForwardBranchesAllBranchesCheckedOutInOtherWorktree,
sync.FetchAndAutoForwardBranchesNone,
sync.FetchAndAutoForwardBranchesOnlyMainBranches,
sync.FetchAndAutoForwardBranchesWorktreeAddedAfterStartup,
sync.FetchPrune,
sync.FetchWhenSortedByDate,
sync.ForcePush,