Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions pkg/commands/git_commands/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
9 changes: 9 additions & 0 deletions pkg/commands/oscommands/cmd_obj.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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...)

Expand Down
4 changes: 4 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}} --",
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/user_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 5 additions & 1 deletion pkg/gui/background.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
25 changes: 11 additions & 14 deletions pkg/gui/controllers/files_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -1189,24 +1189,21 @@ func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error

func (self *FilesController) fetch() error {
return self.c.WithWaitingStatus(self.c.Tr.FetchingStatus, func(task gocui.Task) error {
if err := self.fetchAux(task); err != nil {
return err
self.c.LogAction("Fetch")
err := self.c.Git().Sync.Fetch(task)

if err != nil && strings.Contains(err.Error(), "exit status 128") {
return errors.New(self.c.Tr.PassUnameWrong)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
}

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

if err != nil && strings.Contains(err.Error(), "exit status 128") {
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})
if err == nil {
err = self.c.Helpers().BranchesHelper.AutoForwardBranches()
}

return err
return err
})
}

// Couldn't think of a better term than 'normalised'. Alas.
Expand Down
32 changes: 32 additions & 0 deletions pkg/gui/controllers/helpers/branches_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package helpers

import (
"errors"
"fmt"
"strings"

"github.com/jesseduffield/gocui"
Expand Down Expand Up @@ -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:] {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a use case for auto-forwarding all branches: if you also don't have a use case for it I would just not support it to keep the code simple.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use case is that you collaborate on some branches with a coworker, they push some new commits, and you get them auto-forwarded so that you have those commits right available when you check out the branch again. Saves one pull after checkout. I find this useful and will probably turn it on myself. (The downside is that you don't see that new commits were pushed, so that you can inspect them using "View divergence from upstream").

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
}
8 changes: 8 additions & 0 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ type Actions struct {
RenameBranch string
CreateBranch string
FastForwardBranch string
AutoForwardBranches string
CherryPick string
CheckoutFile string
DiscardOldFileChange string
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -2137,6 +2139,12 @@ gui:
"0.44.0": `- The gui.branchColors config option is deprecated; it will be removed in a future version. Please use gui.branchColorPatterns instead.
- 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.`,
"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.`,
"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:

git:
autoForwardBranches: none

If, on the other hand, you want this even for feature branches, you can set it to 'allBranches' instead.`,
},
}
}
Original file line number Diff line number Diff line change
@@ -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 ✓"),
)
},
})
54 changes: 54 additions & 0 deletions pkg/integration/tests/sync/fetch_and_auto_forward_branches_none.go
Original file line number Diff line number Diff line change
@@ -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("↑"),
)
},
})
Original file line number Diff line number Diff line change
@@ -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 ✓"),
)
},
})
3 changes: 3 additions & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions schema/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down