Add config for auto-forwarding branches after fetching

This commit is contained in:
Stefan Haller 2025-04-18 14:02:11 +02:00
parent 2174762315
commit eaaa937315
14 changed files with 248 additions and 2 deletions

View file

@ -337,6 +337,10 @@ git:
# If true, periodically refresh files and submodules # If true, periodically refresh files and submodules
autoRefresh: true autoRefresh: true
# If not "none", lazygit will automatically forward branches to their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
autoForwardBranches: onlyMainBranches
# If true, pass the --all arg to git fetch # If true, pass the --all arg to git fetch
fetchAll: true fetchAll: true

View file

@ -285,3 +285,11 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
return stdout == "", nil return stdout == "", nil
} }
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
cmdArgs := NewGitCmd("update-ref").
Arg("--stdin").
ToArgv()
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
}

View file

@ -21,6 +21,9 @@ type ICmdObj interface {
// outputs args vector e.g. ["git", "commit", "-m", "my message"] // outputs args vector e.g. ["git", "commit", "-m", "my message"]
Args() []string Args() []string
// Set a string to be used as stdin for the command.
SetStdin(input string) ICmdObj
AddEnvVars(...string) ICmdObj AddEnvVars(...string) ICmdObj
GetEnvVars() []string GetEnvVars() []string
@ -131,6 +134,12 @@ func (self *CmdObj) Args() []string {
return self.cmd.Args return self.cmd.Args
} }
func (self *CmdObj) SetStdin(input string) ICmdObj {
self.cmd.Stdin = strings.NewReader(input)
return self
}
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj { func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
self.cmd.Env = append(self.cmd.Env, vars...) self.cmd.Env = append(self.cmd.Env, vars...)

View file

@ -244,6 +244,9 @@ type GitConfig struct {
AutoFetch bool `yaml:"autoFetch"` AutoFetch bool `yaml:"autoFetch"`
// If true, periodically refresh files and submodules // If true, periodically refresh files and submodules
AutoRefresh bool `yaml:"autoRefresh"` AutoRefresh bool `yaml:"autoRefresh"`
// If not "none", lazygit will automatically forward branches to their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
// If true, pass the --all arg to git fetch // If true, pass the --all arg to git fetch
FetchAll bool `yaml:"fetchAll"` FetchAll bool `yaml:"fetchAll"`
// If true, lazygit will automatically stage files that used to have merge // If true, lazygit will automatically stage files that used to have merge
@ -822,6 +825,7 @@ func GetDefaultConfig() *UserConfig {
MainBranches: []string{"master", "main"}, MainBranches: []string{"master", "main"},
AutoFetch: true, AutoFetch: true,
AutoRefresh: true, AutoRefresh: true,
AutoForwardBranches: "onlyMainBranches",
FetchAll: true, FetchAll: true,
AutoStageResolvedConflicts: true, AutoStageResolvedConflicts: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --", BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",

View file

@ -19,6 +19,10 @@ func (config *UserConfig) Validate() error {
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil { []string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
return err return err
} }
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
return err
}
if err := validateKeybindings(config.Keybinding); err != nil { if err := validateKeybindings(config.Keybinding); err != nil {
return err return err
} }

View file

@ -125,7 +125,11 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
err = self.gui.git.Sync.FetchBackground() err = self.gui.git.Sync.FetchBackground()
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
if err == nil {
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
}
return err return err
} }

View file

@ -1196,7 +1196,11 @@ func (self *FilesController) fetch() error {
return errors.New(self.c.Tr.PassUnameWrong) 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.ASYNC}) _ = 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 err
}) })

View file

@ -2,6 +2,7 @@ package helpers
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
@ -263,3 +264,34 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
} }
return nil return nil
} }
func (self *BranchesHelper) AutoForwardBranches() error {
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
return nil
}
allBranches := self.c.UserConfig().Git.AutoForwardBranches == "allBranches"
branches := self.c.Model().Branches
updateCommands := ""
// The first branch is the currently checked out branch; skip it
for _, branch := range branches[1:] {
if branch.RemoteBranchStoredLocally() && (allBranches || lo.Contains(self.c.UserConfig().Git.MainBranches, branch.Name)) {
isStrictlyBehind := branch.IsBehindForPull() && !branch.IsAheadForPull()
if isStrictlyBehind {
updateCommands += fmt.Sprintf("update %s %s %s\n", branch.FullRefName(), branch.FullUpstreamRefName(), branch.CommitHash)
}
}
}
if updateCommands == "" {
return nil
}
self.c.LogAction(self.c.Tr.Actions.AutoForwardBranches)
self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false)
err := self.c.Git().Branch.UpdateBranchRefs(updateCommands)
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC})
return err
}

View file

@ -931,6 +931,7 @@ type Actions struct {
RenameBranch string RenameBranch string
CreateBranch string CreateBranch string
FastForwardBranch string FastForwardBranch string
AutoForwardBranches string
CherryPick string CherryPick string
CheckoutFile string CheckoutFile string
DiscardOldFileChange string DiscardOldFileChange string
@ -2059,6 +2060,7 @@ func EnglishTranslationSet() *TranslationSet {
MixedReset: "Mixed reset", MixedReset: "Mixed reset",
HardReset: "Hard reset", HardReset: "Hard reset",
FastForwardBranch: "Fast forward branch", FastForwardBranch: "Fast forward branch",
AutoForwardBranches: "Auto-forward branches",
Undo: "Undo", Undo: "Undo",
Redo: "Redo", Redo: "Redo",
CopyPullRequestURL: "Copy pull request URL", CopyPullRequestURL: "Copy pull request URL",

View file

@ -0,0 +1,54 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FetchAndAutoForwardBranchesAllBranches = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Fetch from remote and auto-forward branches with config set to 'allBranches'",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AutoForwardBranches = "allBranches"
},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(3)
shell.NewBranch("feature")
shell.NewBranch("diverged")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("feature", "origin/feature")
shell.SetBranchUpstream("diverged", "origin/diverged")
shell.Checkout("master")
shell.HardReset("HEAD^")
shell.Checkout("feature")
shell.HardReset("HEAD~2")
shell.Checkout("diverged")
shell.HardReset("HEAD~2")
shell.EmptyCommit("local")
shell.NewBranch("checked-out")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master ↓1").DoesNotContain("↑"),
)
t.Views().Files().
IsFocused().
Press(keys.Files.Fetch)
// AutoForwardBranches is "allBranches": both master and feature get forwarded
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ✓"),
Contains("master ✓"),
)
},
})

View file

@ -0,0 +1,54 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FetchAndAutoForwardBranchesNone = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Fetch from remote and auto-forward branches with config set to 'none'",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AutoForwardBranches = "none"
},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(3)
shell.NewBranch("feature")
shell.NewBranch("diverged")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("feature", "origin/feature")
shell.SetBranchUpstream("diverged", "origin/diverged")
shell.Checkout("master")
shell.HardReset("HEAD^")
shell.Checkout("feature")
shell.HardReset("HEAD~2")
shell.Checkout("diverged")
shell.HardReset("HEAD~2")
shell.EmptyCommit("local")
shell.NewBranch("checked-out")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master ↓1").DoesNotContain("↑"),
)
t.Views().Files().
IsFocused().
Press(keys.Files.Fetch)
// AutoForwardBranches is "none": nothing should happen
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master ↓1").DoesNotContain("↑"),
)
},
})

View file

@ -0,0 +1,54 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FetchAndAutoForwardBranchesOnlyMainBranches = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Fetch from remote and auto-forward branches with config set to 'onlyMainBranches'",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AutoForwardBranches = "onlyMainBranches"
},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(3)
shell.NewBranch("feature")
shell.NewBranch("diverged")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("feature", "origin/feature")
shell.SetBranchUpstream("diverged", "origin/diverged")
shell.Checkout("master")
shell.HardReset("HEAD^")
shell.Checkout("feature")
shell.HardReset("HEAD~2")
shell.Checkout("diverged")
shell.HardReset("HEAD~2")
shell.EmptyCommit("local")
shell.NewBranch("checked-out")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master ↓1").DoesNotContain("↑"),
)
t.Views().Files().
IsFocused().
Press(keys.Files.Fetch)
// AutoForwardBranches is "onlyMainBranches": master gets forwarded, but feature doesn't
t.Views().Branches().
Lines(
Contains("checked-out").IsSelected(),
Contains("diverged ↓2↑1"),
Contains("feature ↓2").DoesNotContain("↑"),
Contains("master ✓"),
)
},
})

View file

@ -360,6 +360,9 @@ var tests = []*components.IntegrationTest{
submodule.RemoveNested, submodule.RemoveNested,
submodule.Reset, submodule.Reset,
submodule.ResetFolder, submodule.ResetFolder,
sync.FetchAndAutoForwardBranchesAllBranches,
sync.FetchAndAutoForwardBranchesNone,
sync.FetchAndAutoForwardBranchesOnlyMainBranches,
sync.FetchPrune, sync.FetchPrune,
sync.FetchWhenSortedByDate, sync.FetchWhenSortedByDate,
sync.ForcePush, sync.ForcePush,

View file

@ -325,6 +325,16 @@
"description": "If true, periodically refresh files and submodules", "description": "If true, periodically refresh files and submodules",
"default": true "default": true
}, },
"autoForwardBranches": {
"type": "string",
"enum": [
"none",
"onlyMainBranches",
"allBranches"
],
"description": "If not \"none\", lazygit will automatically forward branches to their upstream after fetching. Applies to branches that are not the currently checked out branch, and only to those that are strictly behind their upstream (as opposed to diverged).\nPossible values: 'none' | 'onlyMainBranches' | 'allBranches'",
"default": "onlyMainBranches"
},
"fetchAll": { "fetchAll": {
"type": "boolean", "type": "boolean",
"description": "If true, pass the --all arg to git fetch", "description": "If true, pass the --all arg to git fetch",