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
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
fetchAll: true

View file

@ -285,3 +285,11 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
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"]
Args() []string
// Set a string to be used as stdin for the command.
SetStdin(input string) ICmdObj
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
@ -131,6 +134,12 @@ func (self *CmdObj) Args() []string {
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 {
self.cmd.Env = append(self.cmd.Env, vars...)

View file

@ -244,6 +244,9 @@ type GitConfig struct {
AutoFetch bool `yaml:"autoFetch"`
// If true, periodically refresh files and submodules
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
FetchAll bool `yaml:"fetchAll"`
// If true, lazygit will automatically stage files that used to have merge
@ -822,6 +825,7 @@ func GetDefaultConfig() *UserConfig {
MainBranches: []string{"master", "main"},
AutoFetch: true,
AutoRefresh: true,
AutoForwardBranches: "onlyMainBranches",
FetchAll: true,
AutoStageResolvedConflicts: true,
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 {
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 {
return err
}

View file

@ -125,7 +125,11 @@ 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}, 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
}

View file

@ -1196,7 +1196,11 @@ 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.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
})

View file

@ -2,6 +2,7 @@ package helpers
import (
"errors"
"fmt"
"strings"
"github.com/jesseduffield/gocui"
@ -263,3 +264,34 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
}
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
CreateBranch string
FastForwardBranch string
AutoForwardBranches string
CherryPick string
CheckoutFile string
DiscardOldFileChange string
@ -2059,6 +2060,7 @@ func EnglishTranslationSet() *TranslationSet {
MixedReset: "Mixed reset",
HardReset: "Hard reset",
FastForwardBranch: "Fast forward branch",
AutoForwardBranches: "Auto-forward branches",
Undo: "Undo",
Redo: "Redo",
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.Reset,
submodule.ResetFolder,
sync.FetchAndAutoForwardBranchesAllBranches,
sync.FetchAndAutoForwardBranchesNone,
sync.FetchAndAutoForwardBranchesOnlyMainBranches,
sync.FetchPrune,
sync.FetchWhenSortedByDate,
sync.ForcePush,

View file

@ -325,6 +325,16 @@
"description": "If true, periodically refresh files and submodules",
"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": {
"type": "boolean",
"description": "If true, pass the --all arg to git fetch",