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:
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,10 +724,10 @@ func (self *RefreshHelper) loadWorktrees() {
|
||||
if err != nil {
|
||||
self.c.Log.Error(err)
|
||||
self.c.Model().Worktrees = []*models.Worktree{}
|
||||
}
|
||||
|
||||
} else {
|
||||
self.c.Model().Worktrees = worktrees
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshWorktrees() {
|
||||
self.loadWorktrees()
|
||||
|
||||
+59
@@ -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()
|
||||
},
|
||||
})
|
||||
@@ -428,6 +428,7 @@ var tests = []*components.IntegrationTest{
|
||||
sync.FetchAndAutoForwardBranchesAllBranchesCheckedOutInOtherWorktree,
|
||||
sync.FetchAndAutoForwardBranchesNone,
|
||||
sync.FetchAndAutoForwardBranchesOnlyMainBranches,
|
||||
sync.FetchAndAutoForwardBranchesWorktreeAddedAfterStartup,
|
||||
sync.FetchPrune,
|
||||
sync.FetchWhenSortedByDate,
|
||||
sync.ForcePush,
|
||||
|
||||
Reference in New Issue
Block a user