Skip to content

Commit bd1e34b

Browse files
authored
Auto-forward main branches after fetching (#4493)
- **PR Description** Add a new user config for auto-forwarding branches after fetching; by default this is set to "onlyMainBranches", but it can be set to "allBranches" to extend it to feature branches too, or to "none" to disable it. This is used both when fetching manually by pressing `f` in the files panel, and for automatic background fetching.
2 parents e4362ee + 7495854 commit bd1e34b

14 files changed

+260
-15
lines changed

docs/Config.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ git:
337337
# If true, periodically refresh files and submodules
338338
autoRefresh: true
339339

340+
# 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).
341+
# Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
342+
autoForwardBranches: onlyMainBranches
343+
340344
# If true, pass the --all arg to git fetch
341345
fetchAll: true
342346

pkg/commands/git_commands/branch.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,11 @@ func (self *BranchCommands) IsBranchMerged(branch *models.Branch, mainBranches *
285285

286286
return stdout == "", nil
287287
}
288+
289+
func (self *BranchCommands) UpdateBranchRefs(updateCommands string) error {
290+
cmdArgs := NewGitCmd("update-ref").
291+
Arg("--stdin").
292+
ToArgv()
293+
294+
return self.cmd.New(cmdArgs).SetStdin(updateCommands).Run()
295+
}

pkg/commands/oscommands/cmd_obj.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ type ICmdObj interface {
2121
// outputs args vector e.g. ["git", "commit", "-m", "my message"]
2222
Args() []string
2323

24+
// Set a string to be used as stdin for the command.
25+
SetStdin(input string) ICmdObj
26+
2427
AddEnvVars(...string) ICmdObj
2528
GetEnvVars() []string
2629

@@ -131,6 +134,12 @@ func (self *CmdObj) Args() []string {
131134
return self.cmd.Args
132135
}
133136

137+
func (self *CmdObj) SetStdin(input string) ICmdObj {
138+
self.cmd.Stdin = strings.NewReader(input)
139+
140+
return self
141+
}
142+
134143
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
135144
self.cmd.Env = append(self.cmd.Env, vars...)
136145

pkg/config/user_config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ type GitConfig struct {
244244
AutoFetch bool `yaml:"autoFetch"`
245245
// If true, periodically refresh files and submodules
246246
AutoRefresh bool `yaml:"autoRefresh"`
247+
// 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).
248+
// Possible values: 'none' | 'onlyMainBranches' | 'allBranches'
249+
AutoForwardBranches string `yaml:"autoForwardBranches" jsonschema:"enum=none,enum=onlyMainBranches,enum=allBranches"`
247250
// If true, pass the --all arg to git fetch
248251
FetchAll bool `yaml:"fetchAll"`
249252
// If true, lazygit will automatically stage files that used to have merge
@@ -822,6 +825,7 @@ func GetDefaultConfig() *UserConfig {
822825
MainBranches: []string{"master", "main"},
823826
AutoFetch: true,
824827
AutoRefresh: true,
828+
AutoForwardBranches: "onlyMainBranches",
825829
FetchAll: true,
826830
AutoStageResolvedConflicts: true,
827831
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",

pkg/config/user_config_validation.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ func (config *UserConfig) Validate() error {
1919
[]string{"none", "onlyArrow", "arrowAndNumber"}); err != nil {
2020
return err
2121
}
22+
if err := validateEnum("git.autoForwardBranches", config.Git.AutoForwardBranches,
23+
[]string{"none", "onlyMainBranches", "allBranches"}); err != nil {
24+
return err
25+
}
2226
if err := validateKeybindings(config.Keybinding); err != nil {
2327
return err
2428
}

pkg/gui/background.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan stru
125125
func (self *BackgroundRoutineMgr) backgroundFetch() (err error) {
126126
err = self.gui.git.Sync.FetchBackground()
127127

128-
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
128+
_ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
129+
130+
if err == nil {
131+
err = self.gui.helpers.BranchesHelper.AutoForwardBranches()
132+
}
129133

130134
return err
131135
}

pkg/gui/controllers/files_controller.go

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,24 +1189,21 @@ func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error
11891189

11901190
func (self *FilesController) fetch() error {
11911191
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
1192-
if err := self.fetchAux(task); err != nil {
1193-
return err
1192+
self.c.LogAction("Fetch")
1193+
err := self.c.Git().Sync.Fetch(task)
1194+
1195+
if err != nil && strings.Contains(err.Error(), "exit status 128") {
1196+
return errors.New(self.c.Tr.PassUnameWrong)
11941197
}
1195-
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
1196-
})
1197-
}
11981198

1199-
func (self *FilesController) fetchAux(task gocui.Task) (err error) {
1200-
self.c.LogAction("Fetch")
1201-
err = self.c.Git().Sync.Fetch(task)
1199+
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.SYNC})
12021200

1203-
if err != nil && strings.Contains(err.Error(), "exit status 128") {
1204-
return errors.New(self.c.Tr.PassUnameWrong)
1205-
}
1206-
1207-
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC})
1201+
if err == nil {
1202+
err = self.c.Helpers().BranchesHelper.AutoForwardBranches()
1203+
}
12081204

1209-
return err
1205+
return err
1206+
})
12101207
}
12111208

12121209
// Couldn't think of a better term than 'normalised'. Alas.

pkg/gui/controllers/helpers/branches_helper.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package helpers
22

33
import (
44
"errors"
5+
"fmt"
56
"strings"
67

78
"github.com/jesseduffield/gocui"
@@ -263,3 +264,34 @@ func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.Remote
263264
}
264265
return nil
265266
}
267+
268+
func (self *BranchesHelper) AutoForwardBranches() error {
269+
if self.c.UserConfig().Git.AutoForwardBranches == "none" {
270+
return nil
271+
}
272+
273+
allBranches := self.c.UserConfig().Git.AutoForwardBranches == "allBranches"
274+
branches := self.c.Model().Branches
275+
updateCommands := ""
276+
// The first branch is the currently checked out branch; skip it
277+
for _, branch := range branches[1:] {
278+
if branch.RemoteBranchStoredLocally() && (allBranches || lo.Contains(self.c.UserConfig().Git.MainBranches, branch.Name)) {
279+
isStrictlyBehind := branch.IsBehindForPull() && !branch.IsAheadForPull()
280+
if isStrictlyBehind {
281+
updateCommands += fmt.Sprintf("update %s %s %s\n", branch.FullRefName(), branch.FullUpstreamRefName(), branch.CommitHash)
282+
}
283+
}
284+
}
285+
286+
if updateCommands == "" {
287+
return nil
288+
}
289+
290+
self.c.LogAction(self.c.Tr.Actions.AutoForwardBranches)
291+
self.c.LogCommand(strings.TrimRight(updateCommands, "\n"), false)
292+
err := self.c.Git().Branch.UpdateBranchRefs(updateCommands)
293+
294+
_ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES}, Mode: types.SYNC})
295+
296+
return err
297+
}

pkg/i18n/english.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,7 @@ type Actions struct {
931931
RenameBranch string
932932
CreateBranch string
933933
FastForwardBranch string
934+
AutoForwardBranches string
934935
CherryPick string
935936
CheckoutFile string
936937
DiscardOldFileChange string
@@ -2059,6 +2060,7 @@ func EnglishTranslationSet() *TranslationSet {
20592060
MixedReset: "Mixed reset",
20602061
HardReset: "Hard reset",
20612062
FastForwardBranch: "Fast forward branch",
2063+
AutoForwardBranches: "Auto-forward branches",
20622064
Undo: "Undo",
20632065
Redo: "Redo",
20642066
CopyPullRequestURL: "Copy pull request URL",
@@ -2137,6 +2139,12 @@ gui:
21372139
"0.44.0": `- The gui.branchColors config option is deprecated; it will be removed in a future version. Please use gui.branchColorPatterns instead.
21382140
- The automatic coloring of branches starting with "feature/", "bugfix/", or "hotfix/" has been removed; if you want this, it's easy to set up using the new gui.branchColorPatterns option.`,
21392141
"0.49.0": `- Executing shell commands (with the ':' prompt) no longer uses an interactive shell, which means that if you want to use your shell aliases in this prompt, you need to do a little bit of setup work. See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands for details.`,
2142+
"0.50.0": `- After fetching, main branches now get auto-forwarded to their upstream if they fall behind. This is useful for keeping your main or master branch up to date automatically. If you don't want this, you can disable it by setting the following in your config:
2143+
2144+
git:
2145+
autoForwardBranches: none
2146+
2147+
If, on the other hand, you want this even for feature branches, you can set it to 'allBranches' instead.`,
21402148
},
21412149
}
21422150
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package sync
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var FetchAndAutoForwardBranchesAllBranches = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Fetch from remote and auto-forward branches with config set to 'allBranches'",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {
13+
config.GetUserConfig().Git.AutoForwardBranches = "allBranches"
14+
},
15+
SetupRepo: func(shell *Shell) {
16+
shell.CreateNCommits(3)
17+
shell.NewBranch("feature")
18+
shell.NewBranch("diverged")
19+
shell.CloneIntoRemote("origin")
20+
shell.SetBranchUpstream("master", "origin/master")
21+
shell.SetBranchUpstream("feature", "origin/feature")
22+
shell.SetBranchUpstream("diverged", "origin/diverged")
23+
shell.Checkout("master")
24+
shell.HardReset("HEAD^")
25+
shell.Checkout("feature")
26+
shell.HardReset("HEAD~2")
27+
shell.Checkout("diverged")
28+
shell.HardReset("HEAD~2")
29+
shell.EmptyCommit("local")
30+
shell.NewBranch("checked-out")
31+
},
32+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
33+
t.Views().Branches().
34+
Lines(
35+
Contains("checked-out").IsSelected(),
36+
Contains("diverged ↓2↑1"),
37+
Contains("feature ↓2").DoesNotContain("↑"),
38+
Contains("master ↓1").DoesNotContain("↑"),
39+
)
40+
41+
t.Views().Files().
42+
IsFocused().
43+
Press(keys.Files.Fetch)
44+
45+
// AutoForwardBranches is "allBranches": both master and feature get forwarded
46+
t.Views().Branches().
47+
Lines(
48+
Contains("checked-out").IsSelected(),
49+
Contains("diverged ↓2↑1"),
50+
Contains("feature ✓"),
51+
Contains("master ✓"),
52+
)
53+
},
54+
})

0 commit comments

Comments
 (0)