summaryrefslogtreecommitdiffstats
path: root/pkg/gui/controllers/sync_controller.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/gui/controllers/sync_controller.go')
-rw-r--r--pkg/gui/controllers/sync_controller.go253
1 files changed, 253 insertions, 0 deletions
diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go
new file mode 100644
index 000000000..a2fda53a1
--- /dev/null
+++ b/pkg/gui/controllers/sync_controller.go
@@ -0,0 +1,253 @@
+package controllers
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/jesseduffield/lazygit/pkg/commands"
+ "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
+ "github.com/jesseduffield/lazygit/pkg/commands/models"
+ "github.com/jesseduffield/lazygit/pkg/config"
+ "github.com/jesseduffield/lazygit/pkg/gui/popup"
+ "github.com/jesseduffield/lazygit/pkg/gui/types"
+)
+
+type SyncController struct {
+ // I've said publicly that I'm against single-letter variable names but in this
+ // case I would actually prefer a _zero_ letter variable name in the form of
+ // struct embedding, but Go does not allow hiding public fields in an embedded struct
+ // to the client
+ c *ControllerCommon
+ git *commands.GitCommand
+
+ getCheckedOutBranch func() *models.Branch
+ suggestionsHelper ISuggestionsHelper
+ getSuggestedRemote func() string
+ checkMergeOrRebase func(error) error
+}
+
+var _ types.IController = &SyncController{}
+
+func NewSyncController(
+ c *ControllerCommon,
+ git *commands.GitCommand,
+ getCheckedOutBranch func() *models.Branch,
+ suggestionsHelper ISuggestionsHelper,
+ getSuggestedRemote func() string,
+ checkMergeOrRebase func(error) error,
+) *SyncController {
+ return &SyncController{
+ c: c,
+ git: git,
+
+ getCheckedOutBranch: getCheckedOutBranch,
+ suggestionsHelper: suggestionsHelper,
+ getSuggestedRemote: getSuggestedRemote,
+ checkMergeOrRebase: checkMergeOrRebase,
+ }
+}
+
+func (self *SyncController) Keybindings(getKey func(key string) interface{}, config config.KeybindingConfig, guards types.KeybindingGuards) []*types.Binding {
+ bindings := []*types.Binding{
+ {
+ Key: getKey(config.Universal.PushFiles),
+ Handler: guards.NoPopupPanel(self.HandlePush),
+ Description: self.c.Tr.LcPush,
+ },
+ {
+ Key: getKey(config.Universal.PullFiles),
+ Handler: guards.NoPopupPanel(self.HandlePull),
+ Description: self.c.Tr.LcPull,
+ },
+ }
+
+ return bindings
+}
+
+func (self *SyncController) Context() types.Context {
+ return nil
+}
+
+func (self *SyncController) HandlePush() error {
+ return self.branchCheckedOut(self.push)()
+}
+
+func (self *SyncController) HandlePull() error {
+ return self.branchCheckedOut(self.pull)()
+}
+
+func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
+ return func() error {
+ currentBranch := self.getCheckedOutBranch()
+ if currentBranch == nil {
+ // need to wait for branches to refresh
+ return nil
+ }
+
+ return f(currentBranch)
+ }
+}
+
+func (self *SyncController) push(currentBranch *models.Branch) error {
+ // if we have pullables we'll ask if the user wants to force push
+ if currentBranch.IsTrackingRemote() {
+ opts := pushOpts{
+ force: false,
+ upstreamRemote: currentBranch.UpstreamRemote,
+ upstreamBranch: currentBranch.UpstreamBranch,
+ }
+ if currentBranch.HasCommitsToPull() {
+ opts.force = true
+ return self.requestToForcePush(opts)
+ } else {
+ return self.pushAux(opts)
+ }
+ } else {
+ if self.git.Config.GetPushToCurrent() {
+ return self.pushAux(pushOpts{setUpstream: true})
+ } else {
+ return self.promptForUpstream(currentBranch, func(upstream string) error {
+ var upstreamBranch, upstreamRemote string
+ split := strings.Split(upstream, " ")
+ if len(split) == 2 {
+ upstreamRemote = split[0]
+ upstreamBranch = split[1]
+ } else {
+ upstreamRemote = upstream
+ upstreamBranch = ""
+ }
+
+ return self.pushAux(pushOpts{
+ force: false,
+ upstreamRemote: upstreamRemote,
+ upstreamBranch: upstreamBranch,
+ setUpstream: true,
+ })
+ })
+ }
+ }
+}
+
+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.promptForUpstream(currentBranch, func(upstream string) error {
+ var upstreamBranch, upstreamRemote string
+ split := strings.Split(upstream, " ")
+ if len(split) != 2 {
+ return self.c.ErrorMsg(self.c.Tr.InvalidUpstream)
+ }
+
+ upstreamRemote = split[0]
+ upstreamBranch = split[1]
+
+ if err := self.git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
+ errorMessage := err.Error()
+ if strings.Contains(errorMessage, "does not exist") {
+ errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
+ }
+ return self.c.ErrorMsg(errorMessage)
+ }
+ return self.PullAux(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, Action: action})
+ })
+ }
+
+ return self.PullAux(PullFilesOptions{UpstreamRemote: currentBranch.UpstreamRemote, UpstreamBranch: currentBranch.UpstreamBranch, Action: action})
+}
+
+func (self *SyncController) promptForUpstream(currentBranch *models.Branch, onConfirm func(string) error) error {
+ suggestedRemote := self.getSuggestedRemote()
+
+ return self.c.Prompt(popup.PromptOpts{
+ Title: self.c.Tr.EnterUpstream,
+ InitialContent: suggestedRemote + " " + currentBranch.Name,
+ FindSuggestionsFunc: self.suggestionsHelper.GetRemoteBranchesSuggestionsFunc(" "),
+ HandleConfirm: onConfirm,
+ })
+}
+
+type PullFilesOptions struct {
+ UpstreamRemote string
+ UpstreamBranch string
+ FastForwardOnly bool
+ Action string
+}
+
+func (self *SyncController) PullAux(opts PullFilesOptions) error {
+ return self.c.WithLoaderPanel(self.c.Tr.PullWait, func() error {
+ return self.pullWithLock(opts)
+ })
+}
+
+func (self *SyncController) pullWithLock(opts PullFilesOptions) error {
+ self.c.LogAction(opts.Action)
+
+ err := self.git.Sync.Pull(
+ git_commands.PullOptions{
+ RemoteName: opts.UpstreamRemote,
+ BranchName: opts.UpstreamBranch,
+ FastForwardOnly: opts.FastForwardOnly,
+ },
+ )
+
+ return self.checkMergeOrRebase(err)
+}
+
+type pushOpts struct {
+ force bool
+ upstreamRemote string
+ upstreamBranch string
+ setUpstream bool
+}
+
+func (self *SyncController) pushAux(opts pushOpts) error {
+ return self.c.WithLoaderPanel(self.c.Tr.PushWait, func() error {
+ self.c.LogAction(self.c.Tr.Actions.Push)
+ err := self.git.Sync.Push(git_commands.PushOpts{
+ Force: opts.force,
+ UpstreamRemote: opts.upstreamRemote,
+ UpstreamBranch: opts.upstreamBranch,
+ SetUpstream: opts.setUpstream,
+ })
+
+ if err != nil {
+ if !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
+ forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
+ if forcePushDisabled {
+ _ = self.c.ErrorMsg(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
+ return nil
+ }
+ _ = self.c.Ask(popup.AskOpts{
+ Title: self.c.Tr.ForcePush,
+ Prompt: self.c.Tr.ForcePushPrompt,
+ HandleConfirm: func() error {
+ newOpts := opts
+ newOpts.force = true
+
+ return self.pushAux(newOpts)
+ },
+ })
+ return nil
+ }
+ _ = self.c.Error(err)
+ }
+ return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
+ })
+}
+
+func (self *SyncController) requestToForcePush(opts pushOpts) error {
+ forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
+ if forcePushDisabled {
+ return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled)
+ }
+
+ return self.c.Ask(popup.AskOpts{
+ Title: self.c.Tr.ForcePush,
+ Prompt: self.c.Tr.ForcePushPrompt,
+ HandleConfirm: func() error {
+ return self.pushAux(opts)
+ },
+ })
+}