diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 819bcca6a..89c903cc5 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -369,11 +369,26 @@ func (c *GitCommand) RebaseBranch(branchName string) error { return c.OSCommand.RunPreparedCommand(cmd) } +type FetchOptions struct { + PromptUserForCredential func(string) string + RemoteName string + BranchName string +} + // Fetch fetch git repo -func (c *GitCommand) Fetch(promptUserForCredential func(string) string, canPromptForCredential bool) error { - return c.OSCommand.DetectUnamePass("git fetch", func(question string) string { - if canPromptForCredential { - return promptUserForCredential(question) +func (c *GitCommand) Fetch(opts FetchOptions) error { + command := "git fetch" + + if opts.RemoteName != "" { + command = fmt.Sprintf("%s %s", command, opts.RemoteName) + } + if opts.BranchName != "" { + command = fmt.Sprintf("%s %s", command, opts.BranchName) + } + + return c.OSCommand.DetectUnamePass(command, func(question string) string { + if opts.PromptUserForCredential != nil { + return opts.PromptUserForCredential(question) } return "\n" }) @@ -430,10 +445,20 @@ func (c *GitCommand) ListStash() (string, error) { return c.OSCommand.RunCommandWithOutput("git stash list") } +type MergeOpts struct { + FastForwardOnly bool +} + // Merge merge -func (c *GitCommand) Merge(branchName string) error { +func (c *GitCommand) Merge(branchName string, opts MergeOpts) error { mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args") - return c.OSCommand.RunCommand("git merge --no-edit %s %s", mergeArgs, branchName) + + command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName) + if opts.FastForwardOnly { + command = fmt.Sprintf("%s --ff-only", command) + } + + return c.OSCommand.RunCommand(command) } // AbortMerge abort merge @@ -487,7 +512,7 @@ func (c *GitCommand) AmendHead() (*exec.Cmd, error) { // Pull pulls from repo func (c *GitCommand) Pull(args string, promptUserForCredential func(string) string) error { - return c.OSCommand.DetectUnamePass("git pull --no-edit "+args, promptUserForCredential) + return c.OSCommand.DetectUnamePass("git pull --no-edit --rebase ", promptUserForCredential) } // PullWithoutPasswordCheck assumes that the pull will not prompt the user for a password diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index 9b0ef5e11..a376e10bf 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -703,7 +703,7 @@ func TestGitCommandMerge(t *testing.T) { return exec.Command("echo") } - assert.NoError(t, gitCmd.Merge("test")) + assert.NoError(t, gitCmd.Merge("test", MergeOpts{})) } // TestGitCommandUsingGpg is a function. diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 3317a8ff4..bfe666b65 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -129,8 +129,12 @@ func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error { return err } go func() { - err := gui.fetch(g, v, true) - gui.HandleCredentialsPopup(g, err) + err := gui.fetch(true) + gui.HandleCredentialsPopup(err) + if err == nil { + _ = gui.closeConfirmationPrompt(gui.g, true) + _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + } }() return nil } @@ -321,7 +325,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error { - err := gui.GitCommand.Merge(branchName) + err := gui.GitCommand.Merge(branchName, commands.MergeOpts{}) return gui.handleGenericMergeCommandResult(err) }, nil) } diff --git a/pkg/gui/credentials_panel.go b/pkg/gui/credentials_panel.go index 3943055a7..905ce0197 100644 --- a/pkg/gui/credentials_panel.go +++ b/pkg/gui/credentials_panel.go @@ -77,7 +77,7 @@ func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error } // HandleCredentialsPopup handles the views after executing a command that might ask for credentials -func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, cmdErr error) { +func (gui *Gui) HandleCredentialsPopup(cmdErr error) { _, _ = gui.g.SetViewOnBottom("credentials") if cmdErr != nil { errMessage := cmdErr.Error() @@ -86,8 +86,5 @@ func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, cmdErr error) { } // we are not logging this error because it may contain a password _ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false) - } else { - _ = gui.closeConfirmationPrompt(g, true) - _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) } } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 437940c1f..1cf69b2ea 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -446,7 +446,7 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error { } for branchName, branch := range conf.Branches { if branchName == currentBranch.Name { - return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName)) + return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name}) } } @@ -459,27 +459,49 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error { } return gui.createErrorPanel(errorMessage) } - return gui.pullFiles(v, "") + return gui.pullFiles(PullFilesOptions{}) }) } - return gui.pullFiles(v, "") + return gui.pullFiles(PullFilesOptions{}) } -func (gui *Gui) pullFiles(v *gocui.View, args string) error { - if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil { +type PullFilesOptions struct { + RemoteName string + BranchName string +} + +func (gui *Gui) pullFiles(opts PullFilesOptions) error { + if err := gui.createLoaderPanel(gui.g, gui.g.CurrentView(), gui.Tr.SLocalize("PullWait")); err != nil { return err } - // we want to first fetch, handling username if it comes up, then either merge or rebase. If merging we might have a merge conflict, likewise if rebasing we might have a conflict too. - // we need a way of saying .then or .catch - - // what if we had a struct which contained an array of functions to run, each of which return a function, or perhaps write to a channel when they're done, and if there is no error, we run the next thing. In this case we first want to fetch, potentially handling a credential popup, then we want to rebase. + strategy := gui.Config.GetUserConfig().GetString("git.pull.mode") go func() { - err := gui.GitCommand.Pull(args, gui.promptUserForCredential) - // gui.handleGenericMergeCommandResult(err) - gui.HandleCredentialsPopup(gui.g, err) + err := gui.GitCommand.Fetch( + commands.FetchOptions{ + PromptUserForCredential: gui.promptUserForCredential, + RemoteName: opts.RemoteName, + BranchName: opts.BranchName, + }, + ) + gui.HandleCredentialsPopup(err) + if err == nil { + switch strategy { + case "rebase": + err := gui.GitCommand.RebaseBranch("FETCH_HEAD") + _ = gui.handleGenericMergeCommandResult(err) + case "merge": + err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{}) + _ = gui.handleGenericMergeCommandResult(err) + case "ff-only": + err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true}) + _ = gui.handleGenericMergeCommandResult(err) + default: + _ = gui.createErrorPanel(fmt.Sprintf("git pull strategy '%s' unrecognised", strategy)) + } + } }() return nil @@ -492,7 +514,11 @@ func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstr go func() { branchName := gui.getCheckedOutBranch().Name err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential) - gui.HandleCredentialsPopup(g, err) + gui.HandleCredentialsPopup(err) + if err == nil { + _ = gui.closeConfirmationPrompt(gui.g, true) + _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) + } }() return nil } diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index d66e56021..3f5d59b59 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -6,6 +6,7 @@ import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -166,16 +167,21 @@ func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error { return nil } -func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (err error) { - err = gui.GitCommand.Fetch(gui.promptUserForCredential, canAskForCredentials) +func (gui *Gui) fetch(canPromptForCredentials bool) (err error) { + fetchOpts := commands.FetchOptions{} + if canPromptForCredentials { + fetchOpts.PromptUserForCredential = gui.promptUserForCredential + } - if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") { + err = gui.GitCommand.Fetch(fetchOpts) + + if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") { colorFunction := color.New(color.FgRed).SprintFunc() coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong"))) close := func(g *gocui.Gui, v *gocui.View) error { return nil } - _ = gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Error"), coloredMessage, close, close) + _ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("Error"), coloredMessage, close, close) } gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC}) diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 0d039de0b..52e24e298 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -501,12 +501,12 @@ func (gui *Gui) startBackgroundFetch() { if !isNew { time.After(60 * time.Second) } - err := gui.fetch(gui.g, gui.g.CurrentView(), false) + err := gui.fetch(false) if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { _ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil) } else { gui.goEvery(time.Second*60, gui.stopChan, func() error { - err := gui.fetch(gui.g, gui.g.CurrentView(), false) + err := gui.fetch(false) return err }) }