package controllers import ( "errors" "fmt" "strings" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type SyncController struct { baseController c *ControllerCommon } var _ types.IController = &SyncController{} func NewSyncController( common *ControllerCommon, ) *SyncController { return &SyncController{ baseController: baseController{}, c: common, } } func (self *SyncController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { Keys: opts.GetKeys(opts.Config.Universal.Push), Handler: opts.Guards.NoPopupPanel(self.HandlePush), GetDisabledReason: self.getDisabledReasonForPushOrPull, Description: self.c.Tr.Push, Tooltip: self.c.Tr.PushTooltip, }, { Keys: opts.GetKeys(opts.Config.Universal.Pull), Handler: opts.Guards.NoPopupPanel(self.HandlePull), GetDisabledReason: self.getDisabledReasonForPushOrPull, Description: self.c.Tr.Pull, Tooltip: self.c.Tr.PullTooltip, }, } return bindings } func (self *SyncController) Context() types.Context { return nil } func (self *SyncController) HandlePush() error { return self.branchCheckedOut(func(branch *models.Branch) error { return self.push(branch, false) })() } // PushBranch pushes the given branch, regardless of whether it's currently // checked out. Unlike HandlePush, this always targets the branch explicitly // (via its own upstream), rather than relying on `git push` implicitly // pushing whatever is checked out. func (self *SyncController) PushBranch(branch *models.Branch) error { return self.push(branch, true) } func (self *SyncController) HandlePull() error { return self.branchCheckedOut(self.pull)() } func (self *SyncController) getDisabledReasonForPushOrPull() *types.DisabledReason { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch != nil { op := self.c.State().GetItemOperation(currentBranch) if op != types.ItemOperationNone { return &types.DisabledReason{Text: self.c.Tr.CantPullOrPushSameBranchTwice} } } return nil } func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error { return func() error { currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch == nil { // need to wait for branches to refresh return nil } return f(currentBranch) } } // explicitTarget forces the push to name the branch and its upstream // explicitly in the refspec, rather than relying on `git push` implicitly // targeting whatever is currently checked out. This is required whenever // currentBranch might not be the checked-out branch (e.g. pushing a branch // selected in the Branches panel). func (self *SyncController) push(currentBranch *models.Branch, explicitTarget bool) error { // if we are behind our upstream branch we'll ask if the user wants to force push if currentBranch.IsTrackingRemote() { opts := pushOpts{remoteBranchStoredLocally: currentBranch.RemoteBranchStoredLocally()} if explicitTarget { opts.upstreamRemote = currentBranch.UpstreamRemote opts.upstreamBranch = currentBranch.UpstreamBranch } if currentBranch.IsBehindForPush() { return self.requestToForcePush(currentBranch, opts) } return self.pushAux(currentBranch, opts) } if !explicitTarget && self.c.Git().Config.GetPushToCurrent() { return self.pushAux(currentBranch, pushOpts{setUpstream: true}) } return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return err } return self.pushAux(currentBranch, pushOpts{ setUpstream: true, upstreamRemote: upstreamRemote, upstreamBranch: upstreamBranch, }) }) } func (self *SyncController) pull(currentBranch *models.Branch) error { action := self.c.Tr.Actions.Pull // if we have no upstream branch we need to set that first if !currentBranch.IsTrackingRemote() { return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { if err := self.setCurrentBranchUpstream(upstream); err != nil { return err } return self.PullAux(currentBranch, PullFilesOptions{Action: action}) }) } return self.PullAux(currentBranch, PullFilesOptions{Action: action}) } // PullBranch pulls the given branch via its linked worktree (which may or // may not be the currently open one). The caller is responsible for // confirming the branch has a linked worktree at all. func (self *SyncController) PullBranch(branch *models.Branch, worktree *models.Worktree) error { action := self.c.Tr.Actions.Pull worktreeGitDir := "" worktreePath := "" if !worktree.IsCurrent { worktreeGitDir = worktree.GitDir worktreePath = worktree.Path } opts := PullFilesOptions{ Action: action, WorktreeGitDir: worktreeGitDir, WorktreePath: worktreePath, IsCurrentWorktree: worktree.IsCurrent, } if !branch.IsTrackingRemote() { return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(branch, func(upstream string) error { upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return err } if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, branch.Name); err != nil { return err } return self.PullAux(branch, opts) }) } return self.PullAux(branch, opts) } func (self *SyncController) setCurrentBranchUpstream(upstream string) error { upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return err } if err := self.c.Git().Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil { if strings.Contains(err.Error(), "does not exist") { return fmt.Errorf( "upstream branch %s/%s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstreamRemote, upstreamBranch, ) } return err } return nil } type PullFilesOptions struct { UpstreamRemote string UpstreamBranch string FastForwardOnly bool Action string // Set when pulling a branch that isn't checked out in the current // worktree, but has its own linked worktree elsewhere. Empty for the // current worktree. WorktreeGitDir string WorktreePath string // False whenever WorktreeGitDir/WorktreePath are set. IsCurrentWorktree bool } func (self *SyncController) PullAux(currentBranch *models.Branch, opts PullFilesOptions) error { return self.c.WithInlineStatus(currentBranch, types.ItemOperationPulling, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error { return self.pullWithLock(task, opts) }) } func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions) error { self.c.LogAction(opts.Action) err := self.c.Git().Sync.Pull( task, git_commands.PullOptions{ RemoteName: opts.UpstreamRemote, BranchName: opts.UpstreamBranch, FastForwardOnly: opts.FastForwardOnly, WorktreeGitDir: opts.WorktreeGitDir, WorktreePath: opts.WorktreePath, }, ) if !opts.IsCurrentWorktree { // We're not looking at this worktree, so we can't drop the user into // an interactive rebase/merge-conflict resolution flow for it - that // UI operates on the currently open worktree. Report the failure // plainly instead and point them at switching to it. if err != nil { return fmt.Errorf("%s\n\n%s", err.Error(), self.c.Tr.PullFailedInOtherWorktree) } self.c.RefreshFromWorker(types.RefreshOptions{}) return nil } return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseAndSelectHeadCommit(err) } type pushOpts struct { force bool forceWithLease bool upstreamRemote string upstreamBranch string setUpstream bool // If this is false, we can't tell ahead of time whether a force-push will // be necessary, so we start with a normal push and offer to force-push if // the server rejected. If this is true, we don't offer to force-push if the // server rejected, but rather ask the user to fetch. remoteBranchStoredLocally bool } func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error { return self.c.WithInlineStatus(currentBranch, types.ItemOperationPushing, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.Push) err := self.c.Git().Sync.Push( task, git_commands.PushOpts{ Force: opts.force, ForceWithLease: opts.forceWithLease, CurrentBranch: currentBranch.Name, UpstreamRemote: opts.upstreamRemote, UpstreamBranch: opts.upstreamBranch, SetUpstream: opts.setUpstream, }) if err != nil { if !opts.force && !opts.forceWithLease && strings.Contains(err.Error(), "Updates were rejected") { if opts.remoteBranchStoredLocally { return errors.New(self.c.Tr.UpdatesRejected) } forcePushDisabled := self.c.UserConfig().Git.DisableForcePushing if forcePushDisabled { return errors.New(self.c.Tr.UpdatesRejectedAndForcePushDisabled) } self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.ForcePush, Prompt: self.forcePushPrompt(), HandleConfirm: func() error { newOpts := opts newOpts.force = true return self.pushAux(currentBranch, newOpts) }, }) return nil } return err } self.c.RefreshFromWorker(types.RefreshOptions{}) return nil }) } func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opts pushOpts) error { forcePushDisabled := self.c.UserConfig().Git.DisableForcePushing if forcePushDisabled { return errors.New(self.c.Tr.ForcePushDisabled) } self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.ForcePush, Prompt: self.forcePushPrompt(), HandleConfirm: func() error { opts.forceWithLease = true return self.pushAux(currentBranch, opts) }, }) return nil } func (self *SyncController) forcePushPrompt() string { return utils.ResolvePlaceholderString( self.c.Tr.ForcePushPrompt, map[string]string{ "cancelKey": self.c.UserConfig().Keybinding.Universal.Return.String(), "confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm.String(), }, ) }