diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index fa60b5ed7..9cf0d740a 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -7,7 +7,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -240,7 +239,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error { HandleConfirm: func() error { gui.c.LogAction(gui.c.Tr.Actions.Merge) err := gui.git.Branch.Merge(branchName, git_commands.MergeOpts{}) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }, }) } @@ -274,7 +273,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error { HandleConfirm: func() error { gui.c.LogAction(gui.c.Tr.Actions.RebaseBranch) err := gui.git.Rebase.RebaseBranch(selectedBranchName) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }, }) } @@ -391,55 +390,6 @@ func (gui *Gui) handleRenameBranch() error { }) } -func (gui *Gui) handleNewBranchOffCurrentItem() error { - ctx := gui.currentSideListContext() - - item, ok := ctx.GetSelectedItem() - if !ok { - return nil - } - - message := utils.ResolvePlaceholderString( - gui.c.Tr.NewBranchNameBranchOff, - map[string]string{ - "branchName": item.Description(), - }, - ) - - prefilledName := "" - if ctx.GetKey() == context.REMOTE_BRANCHES_CONTEXT_KEY { - // will set to the remote's branch name without the remote name - prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1] - } - - return gui.c.Prompt(types.PromptOpts{ - Title: message, - InitialContent: prefilledName, - HandleConfirm: func(response string) error { - gui.c.LogAction(gui.c.Tr.Actions.CreateBranch) - if err := gui.git.Branch.New(sanitizedBranchName(response), item.ID()); err != nil { - return err - } - - // if we're currently in the branch commits context then the selected commit - // is about to go to the top of the list - if ctx.GetKey() == context.BRANCH_COMMITS_CONTEXT_KEY { - ctx.GetPanelState().SetSelectedLineIdx(0) - } - - if ctx.GetKey() != gui.State.Contexts.Branches.GetKey() { - if err := gui.c.PushContext(gui.State.Contexts.Branches); err != nil { - return err - } - } - - gui.State.Panels.Branches.SelectedLineIdx = 0 - - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - }, - }) -} - // sanitizedBranchName will remove all spaces in favor of a dash "-" to meet // git's branch naming requirement. func sanitizedBranchName(input string) string { @@ -454,3 +404,12 @@ func (gui *Gui) handleEnterBranch() error { return gui.switchToSubCommitsContext(branch.RefName()) } + +func (gui *Gui) handleNewBranchOffBranch() error { + selectedBranch := gui.getSelectedBranch() + if selectedBranch == nil { + return nil + } + + return gui.helpers.refs.NewBranch(selectedBranch.RefName(), selectedBranch.RefName(), "") +} diff --git a/pkg/gui/cherry_picking.go b/pkg/gui/cherry_picking.go deleted file mode 100644 index 3cc7fca6d..000000000 --- a/pkg/gui/cherry_picking.go +++ /dev/null @@ -1,187 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -// you can only copy from one context at a time, because the order and position of commits matter - -func (gui *Gui) resetCherryPickingIfNecessary(context types.Context) error { - oldContextKey := types.ContextKey(gui.State.Modes.CherryPicking.ContextKey) - - if oldContextKey != context.GetKey() { - // need to reset the cherry picking mode - gui.State.Modes.CherryPicking.ContextKey = string(context.GetKey()) - gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0) - - return gui.rerenderContextViewIfPresent(oldContextKey) - } - - return nil -} - -func (gui *Gui) handleCopyCommit() error { - // get currently selected commit, add the sha to state. - context := gui.currentSideListContext() - if context == nil { - return nil - } - - if err := gui.resetCherryPickingIfNecessary(context); err != nil { - return err - } - - item, ok := context.GetSelectedItem() - if !ok { - return nil - } - commit, ok := item.(*models.Commit) - if !ok { - return nil - } - - // we will un-copy it if it's already copied - for index, cherryPickedCommit := range gui.State.Modes.CherryPicking.CherryPickedCommits { - if commit.Sha == cherryPickedCommit.Sha { - gui.State.Modes.CherryPicking.CherryPickedCommits = append(gui.State.Modes.CherryPicking.CherryPickedCommits[0:index], gui.State.Modes.CherryPicking.CherryPickedCommits[index+1:]...) - return context.HandleRender() - } - } - - gui.addCommitToCherryPickedCommits(context.GetPanelState().GetSelectedLineIdx()) - return context.HandleRender() -} - -func (gui *Gui) cherryPickedCommitShaMap() map[string]bool { - commitShaMap := map[string]bool{} - for _, commit := range gui.State.Modes.CherryPicking.CherryPickedCommits { - commitShaMap[commit.Sha] = true - } - return commitShaMap -} - -func (gui *Gui) commitsListForContext() []*models.Commit { - ctx := gui.currentSideListContext() - if ctx == nil { - return nil - } - - // using a switch statement, but we should use polymorphism - switch ctx.GetKey() { - case context.BRANCH_COMMITS_CONTEXT_KEY: - return gui.State.Commits - case context.REFLOG_COMMITS_CONTEXT_KEY: - return gui.State.FilteredReflogCommits - case context.SUB_COMMITS_CONTEXT_KEY: - return gui.State.SubCommits - default: - gui.c.Log.Errorf("no commit list for context %s", ctx.GetKey()) - return nil - } -} - -func (gui *Gui) addCommitToCherryPickedCommits(index int) { - commitShaMap := gui.cherryPickedCommitShaMap() - commitsList := gui.commitsListForContext() - commitShaMap[commitsList[index].Sha] = true - - newCommits := []*models.Commit{} - for _, commit := range commitsList { - if commitShaMap[commit.Sha] { - // duplicating just the things we need to put in the rebase TODO list - newCommits = append(newCommits, &models.Commit{Name: commit.Name, Sha: commit.Sha}) - } - } - - gui.State.Modes.CherryPicking.CherryPickedCommits = newCommits -} - -func (gui *Gui) handleCopyCommitRange() error { - // get currently selected commit, add the sha to state. - context := gui.currentSideListContext() - if context == nil { - return nil - } - - if err := gui.resetCherryPickingIfNecessary(context); err != nil { - return err - } - - commitShaMap := gui.cherryPickedCommitShaMap() - commitsList := gui.commitsListForContext() - selectedLineIdx := context.GetPanelState().GetSelectedLineIdx() - - if selectedLineIdx > len(commitsList)-1 { - return nil - } - - // find the last commit that is copied that's above our position - // if there are none, startIndex = 0 - startIndex := 0 - for index, commit := range commitsList[0:selectedLineIdx] { - if commitShaMap[commit.Sha] { - startIndex = index - } - } - - for index := startIndex; index <= selectedLineIdx; index++ { - gui.addCommitToCherryPickedCommits(index) - } - - return context.HandleRender() -} - -// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied -func (gui *Gui) HandlePasteCommits() error { - return gui.c.Ask(types.AskOpts{ - Title: gui.c.Tr.CherryPick, - Prompt: gui.c.Tr.SureCherryPick, - HandleConfirm: func() error { - return gui.c.WithWaitingStatus(gui.c.Tr.CherryPickingStatus, func() error { - gui.c.LogAction(gui.c.Tr.Actions.CherryPick) - err := gui.git.Rebase.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits) - return gui.checkMergeOrRebase(err) - }) - }, - }) -} - -func (gui *Gui) exitCherryPickingMode() error { - contextKey := types.ContextKey(gui.State.Modes.CherryPicking.ContextKey) - - gui.State.Modes.CherryPicking.ContextKey = "" - gui.State.Modes.CherryPicking.CherryPickedCommits = nil - - if contextKey == "" { - gui.c.Log.Warn("context key blank when trying to exit cherry picking mode") - return nil - } - - return gui.rerenderContextViewIfPresent(contextKey) -} - -func (gui *Gui) rerenderContextViewIfPresent(contextKey types.ContextKey) error { - if contextKey == "" { - return nil - } - - context := gui.mustContextForContextKey(contextKey) - - viewName := context.GetViewName() - - view, err := gui.g.View(viewName) - if err != nil { - gui.c.Log.Error(err) - return nil - } - - if types.ContextKey(view.Context) == contextKey { - if err := context.HandleRender(); err != nil { - return err - } - } - - return nil -} diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index e4932d080..8e6410b9e 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -83,7 +83,7 @@ func (gui *Gui) handleDiscardOldFileChange() error { return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error { gui.c.LogAction(gui.c.Tr.Actions.DiscardOldFileChange) if err := gui.git.Rebase.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil { - if err := gui.checkMergeOrRebase(err); err != nil { + if err := gui.helpers.rebase.CheckMergeOrRebase(err); err != nil { return err } } diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 1c115c04b..09b669b04 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -464,13 +464,7 @@ func (gui *Gui) getSideContextSelectedItemId() string { return "" } - item, ok := currentSideContext.GetSelectedItem() - - if ok { - return item.ID() - } - - return "" + return currentSideContext.GetSelectedItemId() } // currently unused diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index 84a5cd67c..dd557f6b2 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -62,7 +62,11 @@ func NewCommitFilesContext( return self } -func (self *CommitFilesContext) GetSelectedItem() (types.ListItem, bool) { - item := self.CommitFileTreeViewModel.GetSelectedFileNode() - return item, item != nil +func (self *CommitFilesContext) GetSelectedItemId() string { + item := self.GetSelectedFileNode() + if item == nil { + return "" + } + + return item.ID() } diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index c7b468972..fc6e1bde1 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -62,6 +62,15 @@ func NewTagsContext( return self } +func (self *TagsContext) GetSelectedItemId() string { + item := self.GetSelectedTag() + if item == nil { + return "" + } + + return item.ID() +} + type TagsViewModel struct { *traits.ListCursor getModel func() []*models.Tag @@ -79,11 +88,6 @@ func (self *TagsViewModel) GetSelectedTag() *models.Tag { return self.getModel()[self.GetSelectedLineIdx()] } -func (self *TagsViewModel) GetSelectedItem() (types.ListItem, bool) { - item := self.GetSelectedTag() - return item, item != nil -} - func NewTagsViewModel(getModel func() []*models.Tag) *TagsViewModel { self := &TagsViewModel{ getModel: getModel, diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index 68f197259..fafddf9e8 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -62,7 +62,11 @@ func NewWorkingTreeContext( return self } -func (self *WorkingTreeContext) GetSelectedItem() (types.ListItem, bool) { - item := self.FileTreeViewModel.GetSelectedFileNode() - return item, item != nil +func (self *WorkingTreeContext) GetSelectedItemId() string { + item := self.GetSelectedFileNode() + if item == nil { + return "" + } + + return item.ID() } diff --git a/pkg/gui/controllers/cherry_pick_helper.go b/pkg/gui/controllers/cherry_pick_helper.go new file mode 100644 index 000000000..3bce03132 --- /dev/null +++ b/pkg/gui/controllers/cherry_pick_helper.go @@ -0,0 +1,156 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CherryPickHelper struct { + c *types.ControllerCommon + + git *commands.GitCommand + + getContexts func() context.ContextTree + getData func() *cherrypicking.CherryPicking + + rebaseHelper *RebaseHelper +} + +// I'm using the analogy of copy+paste in the terminology here because it's intuitively what's going on, +// even if in truth we're running git cherry-pick + +func NewCherryPickHelper( + c *types.ControllerCommon, + git *commands.GitCommand, + getContexts func() context.ContextTree, + getData func() *cherrypicking.CherryPicking, + rebaseHelper *RebaseHelper, +) *CherryPickHelper { + return &CherryPickHelper{ + c: c, + git: git, + getContexts: getContexts, + getData: getData, + rebaseHelper: rebaseHelper, + } +} + +func (self *CherryPickHelper) Copy(commit *models.Commit, commitsList []*models.Commit, context types.Context) error { + if err := self.resetIfNecessary(context); err != nil { + return err + } + + // we will un-copy it if it's already copied + for index, cherryPickedCommit := range self.getData().CherryPickedCommits { + if commit.Sha == cherryPickedCommit.Sha { + self.getData().CherryPickedCommits = append( + self.getData().CherryPickedCommits[0:index], + self.getData().CherryPickedCommits[index+1:]..., + ) + return self.rerender() + } + } + + self.add(commit, commitsList) + return self.rerender() +} + +func (self *CherryPickHelper) CopyRange(selectedIndex int, commitsList []*models.Commit, context types.Context) error { + if err := self.resetIfNecessary(context); err != nil { + return err + } + + commitShaMap := self.CherryPickedCommitShaMap() + + // find the last commit that is copied that's above our position + // if there are none, startIndex = 0 + startIndex := 0 + for index, commit := range commitsList[0:selectedIndex] { + if commitShaMap[commit.Sha] { + startIndex = index + } + } + + for index := startIndex; index <= selectedIndex; index++ { + commit := commitsList[index] + self.add(commit, commitsList) + } + + return self.rerender() +} + +// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied. +// Only to be called from the branch commits controller +func (self *CherryPickHelper) Paste() error { + return self.c.Ask(types.AskOpts{ + Title: self.c.Tr.CherryPick, + Prompt: self.c.Tr.SureCherryPick, + HandleConfirm: func() error { + return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func() error { + self.c.LogAction(self.c.Tr.Actions.CherryPick) + err := self.git.Rebase.CherryPickCommits(self.getData().CherryPickedCommits) + return self.rebaseHelper.CheckMergeOrRebase(err) + }) + }, + }) +} + +func (self *CherryPickHelper) Reset() error { + self.getData().ContextKey = "" + self.getData().CherryPickedCommits = nil + + return self.rerender() +} + +func (self *CherryPickHelper) CherryPickedCommitShaMap() map[string]bool { + commitShaMap := map[string]bool{} + for _, commit := range self.getData().CherryPickedCommits { + commitShaMap[commit.Sha] = true + } + return commitShaMap +} + +func (self *CherryPickHelper) add(selectedCommit *models.Commit, commitsList []*models.Commit) { + commitShaMap := self.CherryPickedCommitShaMap() + commitShaMap[selectedCommit.Sha] = true + + newCommits := []*models.Commit{} + for _, commit := range commitsList { + if commitShaMap[commit.Sha] { + // duplicating just the things we need to put in the rebase TODO list + newCommits = append(newCommits, &models.Commit{Name: commit.Name, Sha: commit.Sha}) + } + } + + self.getData().CherryPickedCommits = newCommits +} + +// you can only copy from one context at a time, because the order and position of commits matter +func (self *CherryPickHelper) resetIfNecessary(context types.Context) error { + oldContextKey := types.ContextKey(self.getData().ContextKey) + + if oldContextKey != context.GetKey() { + // need to reset the cherry picking mode + self.getData().ContextKey = string(context.GetKey()) + self.getData().CherryPickedCommits = make([]*models.Commit, 0) + } + + return nil +} + +func (self *CherryPickHelper) rerender() error { + for _, context := range []types.Context{ + self.getContexts().BranchCommits, + self.getContexts().ReflogCommits, + self.getContexts().SubCommits, + } { + if err := self.c.PostRefreshUpdate(context); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 9e1cacca7..3e30619be 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -24,17 +24,19 @@ type ( ) type LocalCommitsController struct { - c *types.ControllerCommon - getContext func() types.IListContext - os *oscommands.OSCommand - git *commands.GitCommand - tagsHelper *TagsHelper - refsHelper IRefsHelper + c *types.ControllerCommon + getContext func() types.IListContext + os *oscommands.OSCommand + git *commands.GitCommand + tagsHelper *TagsHelper + refsHelper IRefsHelper + cherryPickHelper *CherryPickHelper + rebaseHelper *RebaseHelper getSelectedLocalCommit func() *models.Commit getCommits func() []*models.Commit getSelectedLocalCommitIdx func() int - checkMergeOrRebase CheckMergeOrRebase + CheckMergeOrRebase CheckMergeOrRebase pullFiles PullFilesFn getHostingServiceMgr GetHostingServiceMgrFn switchToCommitFilesContext SwitchToCommitFilesContextFn @@ -54,10 +56,12 @@ func NewLocalCommitsController( git *commands.GitCommand, tagsHelper *TagsHelper, refsHelper IRefsHelper, + cherryPickHelper *CherryPickHelper, + rebaseHelper *RebaseHelper, getSelectedLocalCommit func() *models.Commit, getCommits func() []*models.Commit, getSelectedLocalCommitIdx func() int, - checkMergeOrRebase CheckMergeOrRebase, + CheckMergeOrRebase CheckMergeOrRebase, pullFiles PullFilesFn, getHostingServiceMgr GetHostingServiceMgrFn, switchToCommitFilesContext SwitchToCommitFilesContextFn, @@ -74,10 +78,12 @@ func NewLocalCommitsController( git: git, tagsHelper: tagsHelper, refsHelper: refsHelper, + cherryPickHelper: cherryPickHelper, + rebaseHelper: rebaseHelper, getSelectedLocalCommit: getSelectedLocalCommit, getCommits: getCommits, getSelectedLocalCommitIdx: getSelectedLocalCommitIdx, - checkMergeOrRebase: checkMergeOrRebase, + CheckMergeOrRebase: CheckMergeOrRebase, pullFiles: pullFiles, getHostingServiceMgr: getHostingServiceMgr, switchToCommitFilesContext: switchToCommitFilesContext, @@ -160,6 +166,27 @@ func (self *LocalCommitsController) Keybindings( Handler: self.checkSelected(self.handleCommitRevert), Description: self.c.Tr.LcRevertCommit, }, + { + Key: getKey(config.Universal.New), + Modifier: gocui.ModNone, + Handler: self.checkSelected(self.newBranch), + Description: self.c.Tr.LcCreateNewBranchFromCommit, + }, + { + Key: getKey(config.Commits.CherryPickCopy), + Handler: self.checkSelected(self.copy), + Description: self.c.Tr.LcCherryPickCopy, + }, + { + Key: getKey(config.Commits.CherryPickCopyRange), + Handler: self.checkSelected(self.copyRange), + Description: self.c.Tr.LcCherryPickCopyRange, + }, + { + Key: getKey(config.Commits.PasteCommits), + Handler: guards.OutsideFilterMode(self.paste), + Description: self.c.Tr.LcPasteCommits, + }, // overriding these navigation keybindings because we might need to load // more commits on demand { @@ -380,7 +407,7 @@ func (self *LocalCommitsController) pick() error { func (self *LocalCommitsController) interactiveRebase(action string) error { err := self.git.Rebase.InteractiveRebase(self.getCommits(), self.getSelectedLocalCommitIdx(), action) - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) } // handleMidRebaseCommand sees if the selected commit is in fact a rebasing @@ -448,7 +475,7 @@ func (self *LocalCommitsController) handleCommitMoveDown() error { // TODO: use MoveSelectedLine _ = self.getContext().HandleNextLine() } - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) }) } @@ -483,7 +510,7 @@ func (self *LocalCommitsController) handleCommitMoveUp() error { if err == nil { _ = self.getContext().HandlePrevLine() } - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) }) } @@ -495,7 +522,7 @@ func (self *LocalCommitsController) handleCommitAmendTo() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.AmendCommit) err := self.git.Rebase.AmendTo(self.getSelectedLocalCommit().Sha) - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) }) }, }) @@ -601,7 +628,7 @@ func (self *LocalCommitsController) handleSquashAllAboveFixupCommits(commit *mod return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) err := self.git.Rebase.SquashAllAboveFixupCommits(commit.Sha) - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) }) }, }) @@ -781,3 +808,19 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) func (self *LocalCommitsController) Context() types.Context { return self.getContext() } + +func (self *LocalCommitsController) newBranch(commit *models.Commit) error { + return self.refsHelper.NewBranch(commit.RefName(), commit.Description(), "") +} + +func (self *LocalCommitsController) copy(commit *models.Commit) error { + return self.cherryPickHelper.Copy(commit, self.getCommits(), self.getContext()) +} + +func (self *LocalCommitsController) copyRange(*models.Commit) error { + return self.cherryPickHelper.CopyRange(self.getContext().GetPanelState().GetSelectedLineIdx(), self.getCommits(), self.getContext()) +} + +func (self *LocalCommitsController) paste() error { + return self.cherryPickHelper.Paste() +} diff --git a/pkg/gui/controllers/rebase_helper.go b/pkg/gui/controllers/rebase_helper.go new file mode 100644 index 000000000..6515895c1 --- /dev/null +++ b/pkg/gui/controllers/rebase_helper.go @@ -0,0 +1,192 @@ +package controllers + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type RebaseHelper struct { + c *types.ControllerCommon + getContexts func() context.ContextTree + git *commands.GitCommand + takeOverMergeConflictScrolling func() +} + +func NewRebaseHelper( + c *types.ControllerCommon, + getContexts func() context.ContextTree, + git *commands.GitCommand, + takeOverMergeConflictScrolling func(), +) *RebaseHelper { + return &RebaseHelper{ + c: c, + getContexts: getContexts, + git: git, + takeOverMergeConflictScrolling: takeOverMergeConflictScrolling, + } +} + +type RebaseOption string + +const ( + REBASE_OPTION_CONTINUE string = "continue" + REBASE_OPTION_ABORT string = "abort" + REBASE_OPTION_SKIP string = "skip" +) + +func (self *RebaseHelper) CreateRebaseOptionsMenu() error { + options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT} + + if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + options = append(options, REBASE_OPTION_SKIP) + } + + menuItems := make([]*types.MenuItem, len(options)) + for i, option := range options { + // note to self. Never, EVER, close over loop variables in a function + option := option + menuItems[i] = &types.MenuItem{ + DisplayString: option, + OnPress: func() error { + return self.genericMergeCommand(option) + }, + } + } + + var title string + if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { + title = self.c.Tr.MergeOptionsTitle + } else { + title = self.c.Tr.RebaseOptionsTitle + } + + return self.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems}) +} + +func (self *RebaseHelper) genericMergeCommand(command string) error { + status := self.git.Status.WorkingTreeState() + + if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { + return self.c.ErrorMsg(self.c.Tr.NotMergingOrRebasing) + } + + self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command)) + + commandType := "" + switch status { + case enums.REBASE_MODE_MERGING: + commandType = "merge" + case enums.REBASE_MODE_REBASING: + commandType = "rebase" + default: + // shouldn't be possible to land here + } + + // we should end up with a command like 'git merge --continue' + + // it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge + if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit { + // TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction + return self.c.RunSubprocessAndRefresh( + self.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), + ) + } + result := self.git.Rebase.GenericMergeOrRebaseAction(commandType, command) + if err := self.CheckMergeOrRebase(result); err != nil { + return err + } + return nil +} + +var conflictStrings = []string{ + "Failed to merge in the changes", + "When you have resolved this problem", + "fix conflicts", + "Resolve all conflicts manually", +} + +func isMergeConflictErr(errStr string) bool { + for _, str := range conflictStrings { + if strings.Contains(errStr, str) { + return true + } + } + + return false +} + +func (self *RebaseHelper) CheckMergeOrRebase(result error) error { + if err := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil { + return err + } + if result == nil { + return nil + } else if strings.Contains(result.Error(), "No changes - did you forget to use") { + return self.genericMergeCommand(REBASE_OPTION_SKIP) + } else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") { + return self.genericMergeCommand(REBASE_OPTION_CONTINUE) + } else if strings.Contains(result.Error(), "No rebase in progress?") { + // assume in this case that we're already done + return nil + } else if isMergeConflictErr(result.Error()) { + return self.c.Ask(types.AskOpts{ + Title: self.c.Tr.FoundConflictsTitle, + Prompt: self.c.Tr.FoundConflicts, + HandlersManageFocus: true, + HandleConfirm: func() error { + return self.c.PushContext(self.getContexts().Files) + }, + HandleClose: func() error { + if err := self.c.PopContext(); err != nil { + return err + } + + return self.genericMergeCommand(REBASE_OPTION_ABORT) + }, + }) + } else { + return self.c.ErrorMsg(result.Error()) + } +} + +func (self *RebaseHelper) AbortMergeOrRebaseWithConfirm() error { + // prompt user to confirm that they want to abort, then do it + mode := self.workingTreeStateNoun() + return self.c.Ask(types.AskOpts{ + Title: fmt.Sprintf(self.c.Tr.AbortTitle, mode), + Prompt: fmt.Sprintf(self.c.Tr.AbortPrompt, mode), + HandleConfirm: func() error { + return self.genericMergeCommand(REBASE_OPTION_ABORT) + }, + }) +} + +func (self *RebaseHelper) workingTreeStateNoun() string { + workingTreeState := self.git.Status.WorkingTreeState() + switch workingTreeState { + case enums.REBASE_MODE_NONE: + return "" + case enums.REBASE_MODE_MERGING: + return "merge" + default: + return "rebase" + } +} + +// PromptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress +func (self *RebaseHelper) PromptToContinueRebase() error { + self.takeOverMergeConflictScrolling() + + return self.c.Ask(types.AskOpts{ + Title: "continue", + Prompt: self.c.Tr.ConflictsResolved, + HandleConfirm: func() error { + return self.genericMergeCommand(REBASE_OPTION_CONTINUE) + }, + }) +} diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index e84e3f731..582d8a9de 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -22,7 +22,7 @@ type SyncController struct { getCheckedOutBranch func() *models.Branch suggestionsHelper ISuggestionsHelper getSuggestedRemote func() string - checkMergeOrRebase func(error) error + CheckMergeOrRebase func(error) error } var _ types.IController = &SyncController{} @@ -33,7 +33,7 @@ func NewSyncController( getCheckedOutBranch func() *models.Branch, suggestionsHelper ISuggestionsHelper, getSuggestedRemote func() string, - checkMergeOrRebase func(error) error, + CheckMergeOrRebase func(error) error, ) *SyncController { return &SyncController{ c: c, @@ -42,7 +42,7 @@ func NewSyncController( getCheckedOutBranch: getCheckedOutBranch, suggestionsHelper: suggestionsHelper, getSuggestedRemote: getSuggestedRemote, - checkMergeOrRebase: checkMergeOrRebase, + CheckMergeOrRebase: CheckMergeOrRebase, } } @@ -191,7 +191,7 @@ func (self *SyncController) pullWithLock(opts PullFilesOptions) error { }, ) - return self.checkMergeOrRebase(err) + return self.CheckMergeOrRebase(err) } type pushOpts struct { diff --git a/pkg/gui/controllers/types.go b/pkg/gui/controllers/types.go index ec0b71a90..c3466e536 100644 --- a/pkg/gui/controllers/types.go +++ b/pkg/gui/controllers/types.go @@ -9,6 +9,7 @@ type IRefsHelper interface { CheckoutRef(ref string, options types.CheckoutRefOptions) error CreateGitResetMenu(ref string) error ResetToRef(ref string, strength string, envVars []string) error + NewBranch(from string, fromDescription string, suggestedBranchname string) error } type ISuggestionsHelper interface { diff --git a/pkg/gui/diffing.go b/pkg/gui/diffing.go index 385139b76..9b636e196 100644 --- a/pkg/gui/diffing.go +++ b/pkg/gui/diffing.go @@ -53,15 +53,11 @@ func (gui *Gui) currentDiffTerminals() []string { } return nil default: - context := gui.currentSideListContext() - if context == nil { + itemId := gui.getSideContextSelectedItemId() + if itemId == "" { return nil } - item, ok := context.GetSelectedItem() - if !ok { - return nil - } - return []string{item.ID()} + return []string{itemId} } } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 1ec32e708..0ffdcfc6c 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -4,7 +4,6 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/filetree" - "github.com/jesseduffield/lazygit/pkg/gui/types" ) // list panel functions @@ -77,19 +76,6 @@ func (gui *Gui) filesRenderToMain() error { return gui.refreshMainViews(refreshOpts) } -// promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress -func (gui *Gui) promptToContinueRebase() error { - gui.takeOverMergeConflictScrolling() - - return gui.PopupHandler.Ask(types.AskOpts{ - Title: "continue", - Prompt: gui.Tr.ConflictsResolved, - HandleConfirm: func() error { - return gui.genericMergeCommand(REBASE_OPTION_CONTINUE) - }, - }) -} - func (gui *Gui) onFocusFile() error { gui.takeOverMergeConflictScrolling() return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 93b9a2862..490ffca53 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -74,6 +74,8 @@ type Helpers struct { files *FilesHelper workingTree *WorkingTreeHelper tags *controllers.TagsHelper + rebase *controllers.RebaseHelper + cherryPick *controllers.CherryPickHelper } type Repo string @@ -373,7 +375,7 @@ const ( type Modes struct { Filtering filtering.Filtering - CherryPicking cherrypicking.CherryPicking + CherryPicking *cherrypicking.CherryPicking Diffing diffing.Diffing } @@ -556,10 +558,12 @@ func (gui *Gui) setControllers() { getState := func() *GuiRepoState { return gui.State } getContexts := func() context.ContextTree { return gui.State.Contexts } // TODO: have a getGit function too + rebaseHelper := controllers.NewRebaseHelper(controllerCommon, getContexts, gui.git, gui.takeOverMergeConflictScrolling) gui.helpers = &Helpers{ refs: NewRefsHelper( controllerCommon, gui.git, + getContexts, getState, ), bisect: controllers.NewBisectHelper(controllerCommon, gui.git), @@ -567,6 +571,14 @@ func (gui *Gui) setControllers() { files: NewFilesHelper(controllerCommon, gui.git, osCommand), workingTree: NewWorkingTreeHelper(func() []*models.File { return gui.State.Files }), tags: controllers.NewTagsHelper(controllerCommon, gui.git), + rebase: rebaseHelper, + cherryPick: controllers.NewCherryPickHelper( + controllerCommon, + gui.git, + getContexts, + func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking }, + rebaseHelper, + ), } syncController := controllers.NewSyncController( @@ -575,7 +587,7 @@ func (gui *Gui) setControllers() { gui.getCheckedOutBranch, gui.helpers.suggestions, gui.getSuggestedRemote, - gui.checkMergeOrRebase, + gui.helpers.rebase.CheckMergeOrRebase, ) gui.Controllers = Controllers{ @@ -624,10 +636,12 @@ func (gui *Gui) setControllers() { gui.git, gui.helpers.tags, gui.helpers.refs, + gui.helpers.cherryPick, + gui.helpers.rebase, gui.getSelectedLocalCommit, func() []*models.Commit { return gui.State.Commits }, func() int { return gui.State.Panels.Commits.SelectedLineIdx }, - gui.checkMergeOrRebase, + gui.helpers.rebase.CheckMergeOrRebase, syncController.HandlePull, gui.getHostingServiceMgr, gui.SwitchToCommitFilesContext, diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4d8eddb48..123e6f5b5 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -276,7 +276,7 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { { ViewName: "", Key: gui.getKey(config.Universal.CreateRebaseOptionsMenu), - Handler: gui.handleCreateRebaseOptionsMenu, + Handler: gui.helpers.rebase.CreateRebaseOptionsMenu, Description: gui.c.Tr.ViewMergeRebaseOptions, OpensMenu: true, }, @@ -423,7 +423,7 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { ViewName: "branches", Contexts: []string{string(context.LOCAL_BRANCHES_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.New), - Handler: gui.handleNewBranchOffCurrentItem, + Handler: gui.handleNewBranchOffBranch, Description: gui.c.Tr.LcNewBranch, }, { @@ -513,13 +513,6 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Handler: gui.handleEnterRemoteBranch, Description: gui.c.Tr.LcViewCommits, }, - { - ViewName: "commits", - Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, - Key: gui.getKey(config.Commits.CherryPickCopy), - Handler: gui.handleCopyCommit, - Description: gui.c.Tr.LcCherryPickCopy, - }, { ViewName: "commits", Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, @@ -527,33 +520,11 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Handler: gui.handleCopySelectedSideContextItemToClipboard, Description: gui.c.Tr.LcCopyCommitShaToClipboard, }, - { - ViewName: "commits", - Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, - Key: gui.getKey(config.Commits.CherryPickCopyRange), - Handler: gui.handleCopyCommitRange, - Description: gui.c.Tr.LcCherryPickCopyRange, - }, - { - ViewName: "commits", - Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, - Key: gui.getKey(config.Commits.PasteCommits), - Handler: guards.OutsideFilterMode(gui.HandlePasteCommits), - Description: gui.c.Tr.LcPasteCommits, - }, - { - ViewName: "commits", - Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, - Key: gui.getKey(config.Universal.New), - Modifier: gocui.ModNone, - Handler: gui.handleNewBranchOffCurrentItem, - Description: gui.c.Tr.LcCreateNewBranchFromCommit, - }, { ViewName: "commits", Contexts: []string{string(context.BRANCH_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.ResetCherryPick), - Handler: gui.exitCherryPickingMode, + Handler: gui.helpers.cherryPick.Reset, Description: gui.c.Tr.LcResetCherryPick, }, { @@ -582,21 +553,21 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { ViewName: "commits", Contexts: []string{string(context.REFLOG_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.CherryPickCopy), - Handler: guards.OutsideFilterMode(gui.handleCopyCommit), + Handler: guards.OutsideFilterMode(gui.handleCopyReflogCommit), Description: gui.c.Tr.LcCherryPickCopy, }, { ViewName: "commits", Contexts: []string{string(context.REFLOG_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.CherryPickCopyRange), - Handler: guards.OutsideFilterMode(gui.handleCopyCommitRange), + Handler: guards.OutsideFilterMode(gui.handleCopyReflogCommitRange), Description: gui.c.Tr.LcCherryPickCopyRange, }, { ViewName: "commits", Contexts: []string{string(context.REFLOG_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.ResetCherryPick), - Handler: gui.exitCherryPickingMode, + Handler: gui.helpers.cherryPick.Reset, Description: gui.c.Tr.LcResetCherryPick, }, { @@ -632,28 +603,28 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { ViewName: "branches", Contexts: []string{string(context.SUB_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.New), - Handler: gui.handleNewBranchOffCurrentItem, + Handler: gui.handleNewBranchOffSubCommit, Description: gui.c.Tr.LcNewBranch, }, { ViewName: "branches", Contexts: []string{string(context.SUB_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.CherryPickCopy), - Handler: gui.handleCopyCommit, + Handler: gui.handleCopySubCommit, Description: gui.c.Tr.LcCherryPickCopy, }, { ViewName: "branches", Contexts: []string{string(context.SUB_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.CherryPickCopyRange), - Handler: gui.handleCopyCommitRange, + Handler: gui.handleCopySubCommitRange, Description: gui.c.Tr.LcCherryPickCopyRange, }, { ViewName: "branches", Contexts: []string{string(context.SUB_COMMITS_CONTEXT_KEY)}, Key: gui.getKey(config.Commits.ResetCherryPick), - Handler: gui.exitCherryPickingMode, + Handler: gui.helpers.cherryPick.Reset, Description: gui.c.Tr.LcResetCherryPick, }, { @@ -690,7 +661,7 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { { ViewName: "stash", Key: gui.getKey(config.Universal.New), - Handler: gui.handleNewBranchOffCurrentItem, + Handler: gui.handleNewBranchOffStashEntry, Description: gui.c.Tr.LcNewBranch, }, { @@ -1220,14 +1191,14 @@ func (gui *Gui) GetInitialKeybindings() []*types.Binding { Contexts: []string{string(context.REMOTE_BRANCHES_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.Select), // gonna use the exact same handler as the 'n' keybinding because everybody wants this to happen when they checkout a remote branch - Handler: gui.handleNewBranchOffCurrentItem, + Handler: gui.handleNewBranchOffRemoteBranch, Description: gui.c.Tr.LcCheckout, }, { ViewName: "branches", Contexts: []string{string(context.REMOTE_BRANCHES_CONTEXT_KEY)}, Key: gui.getKey(config.Universal.New), - Handler: gui.handleNewBranchOffCurrentItem, + Handler: gui.handleNewBranchOffRemoteBranch, Description: gui.c.Tr.LcNewBranch, }, { diff --git a/pkg/gui/list_context.go b/pkg/gui/list_context.go index fbdf78c50..f2da7aaac 100644 --- a/pkg/gui/list_context.go +++ b/pkg/gui/list_context.go @@ -16,9 +16,8 @@ type ListContext struct { OnRenderToMain func(...types.OnFocusOpts) error OnFocusLost func() error - // the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection) - SelectedItem func() (types.ListItem, bool) - OnGetPanelState func() types.IListPanelState + OnGetSelectedItemId func() string + OnGetPanelState func() types.IListPanelState // if this is true, we'll call GetDisplayStrings for just the visible part of the // view and re-render that. This is useful when you need to render different // content based on the selection (e.g. for showing the selected commit) @@ -56,8 +55,8 @@ func formatListFooter(selectedLineIdx int, length int) string { return fmt.Sprintf("%d of %d", selectedLineIdx+1, length) } -func (self *ListContext) GetSelectedItem() (types.ListItem, bool) { - return self.SelectedItem() +func (self *ListContext) GetSelectedItemId() string { + return self.OnGetSelectedItemId() } // OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go index 8137ace82..cbbf98cca 100644 --- a/pkg/gui/list_context_config.go +++ b/pkg/gui/list_context_config.go @@ -63,9 +63,12 @@ func (gui *Gui) branchesListContext() types.IListContext { GetDisplayStrings: func(startIdx int, length int) [][]string { return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedBranch() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } @@ -85,9 +88,12 @@ func (gui *Gui) remotesListContext() types.IListContext { GetDisplayStrings: func(startIdx int, length int) [][]string { return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedRemote() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } @@ -107,9 +113,12 @@ func (gui *Gui) remoteBranchesListContext() types.IListContext { GetDisplayStrings: func(startIdx int, length int) [][]string { return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedRemoteBranch() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } @@ -163,7 +172,7 @@ func (gui *Gui) branchCommitsListContext() types.IListContext { return presentation.GetCommitListDisplayStrings( gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, - gui.cherryPickedCommitShaMap(), + gui.helpers.cherryPick.CherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref, parseEmoji, selectedCommitSha, @@ -173,9 +182,12 @@ func (gui *Gui) branchCommitsListContext() types.IListContext { gui.State.BisectInfo, ) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedLocalCommit() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, RenderSelection: true, } @@ -205,7 +217,7 @@ func (gui *Gui) subCommitsListContext() types.IListContext { return presentation.GetCommitListDisplayStrings( gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, - gui.cherryPickedCommitShaMap(), + gui.helpers.cherryPick.CherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref, parseEmoji, selectedCommitSha, @@ -215,9 +227,12 @@ func (gui *Gui) subCommitsListContext() types.IListContext { git_commands.NewNullBisectInfo(), ) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedSubCommit() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, RenderSelection: true, } @@ -259,14 +274,17 @@ func (gui *Gui) reflogCommitsListContext() types.IListContext { return presentation.GetReflogCommitListDisplayStrings( gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, - gui.cherryPickedCommitShaMap(), + gui.helpers.cherryPick.CherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref, parseEmoji, ) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedReflogCommit() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } @@ -286,9 +304,12 @@ func (gui *Gui) stashListContext() types.IListContext { GetDisplayStrings: func(startIdx int, length int) [][]string { return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedStashEntry() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } @@ -332,9 +353,12 @@ func (gui *Gui) submodulesListContext() types.IListContext { GetDisplayStrings: func(startIdx int, length int) [][]string { return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules) }, - SelectedItem: func() (types.ListItem, bool) { + OnGetSelectedItemId: func() string { item := gui.getSelectedSubmodule() - return item, item != nil + if item == nil { + return "" + } + return item.ID() }, } } diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go index 0f2af6192..2936a560e 100644 --- a/pkg/gui/modes.go +++ b/pkg/gui/modes.go @@ -61,7 +61,7 @@ func (gui *Gui) modeStatuses() []modeStatus { style.FgCyan, ) }, - reset: gui.exitCherryPickingMode, + reset: gui.helpers.cherryPick.Reset, }, { isActive: func() bool { @@ -73,7 +73,7 @@ func (gui *Gui) modeStatuses() []modeStatus { formatWorkingTreeState(workingTreeState), style.FgYellow, ) }, - reset: gui.abortMergeOrRebaseWithConfirm, + reset: gui.helpers.rebase.AbortMergeOrRebaseWithConfirm, }, { isActive: func() bool { diff --git a/pkg/gui/modes/cherrypicking/cherry_picking.go b/pkg/gui/modes/cherrypicking/cherry_picking.go index 705735510..bd5c6437a 100644 --- a/pkg/gui/modes/cherrypicking/cherry_picking.go +++ b/pkg/gui/modes/cherrypicking/cherry_picking.go @@ -11,8 +11,8 @@ type CherryPicking struct { ContextKey string } -func New() CherryPicking { - return CherryPicking{ +func New() *CherryPicking { + return &CherryPicking{ CherryPickedCommits: make([]*models.Commit, 0), ContextKey: "", } diff --git a/pkg/gui/patch_options_panel.go b/pkg/gui/patch_options_panel.go index 1ae0693f1..c0333dad1 100644 --- a/pkg/gui/patch_options_panel.go +++ b/pkg/gui/patch_options_panel.go @@ -102,7 +102,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error { commitIndex := gui.getPatchCommitIndex() gui.c.LogAction(gui.c.Tr.Actions.RemovePatchFromCommit) err := gui.git.Patch.DeletePatchesFromCommit(gui.State.Commits, commitIndex) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }) } @@ -119,7 +119,7 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error { commitIndex := gui.getPatchCommitIndex() gui.c.LogAction(gui.c.Tr.Actions.MovePatchToSelectedCommit) err := gui.git.Patch.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }) } @@ -137,7 +137,7 @@ func (gui *Gui) handleMovePatchIntoWorkingTree() error { commitIndex := gui.getPatchCommitIndex() gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoIndex) err := gui.git.Patch.MovePatchIntoIndex(gui.State.Commits, commitIndex, stash) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }) } @@ -167,7 +167,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error { commitIndex := gui.getPatchCommitIndex() gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoNewCommit) err := gui.git.Patch.PullPatchIntoNewCommit(gui.State.Commits, commitIndex) - return gui.checkMergeOrRebase(err) + return gui.helpers.rebase.CheckMergeOrRebase(err) }) } diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go deleted file mode 100644 index 897c389f8..000000000 --- a/pkg/gui/rebase_options_panel.go +++ /dev/null @@ -1,156 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -type RebaseOption string - -const ( - REBASE_OPTION_CONTINUE = "continue" - REBASE_OPTION_ABORT = "abort" - REBASE_OPTION_SKIP = "skip" -) - -func (gui *Gui) handleCreateRebaseOptionsMenu() error { - options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT} - - if gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { - options = append(options, REBASE_OPTION_SKIP) - } - - menuItems := make([]*types.MenuItem, len(options)) - for i, option := range options { - // note to self. Never, EVER, close over loop variables in a function - option := option - menuItems[i] = &types.MenuItem{ - DisplayString: option, - OnPress: func() error { - return gui.genericMergeCommand(option) - }, - } - } - - var title string - if gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { - title = gui.c.Tr.MergeOptionsTitle - } else { - title = gui.c.Tr.RebaseOptionsTitle - } - - return gui.c.Menu(types.CreateMenuOptions{Title: title, Items: menuItems}) -} - -func (gui *Gui) genericMergeCommand(command string) error { - status := gui.git.Status.WorkingTreeState() - - if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { - return gui.c.ErrorMsg(gui.c.Tr.NotMergingOrRebasing) - } - - gui.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command)) - - commandType := "" - switch status { - case enums.REBASE_MODE_MERGING: - commandType = "merge" - case enums.REBASE_MODE_REBASING: - commandType = "rebase" - default: - // shouldn't be possible to land here - } - - // we should end up with a command like 'git merge --continue' - - // it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge - if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.c.UserConfig.Git.Merging.ManualCommit { - // TODO: see if we should be calling more of the code from gui.Git.Rebase.GenericMergeOrRebaseAction - return gui.runSubprocessWithSuspenseAndRefresh( - gui.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), - ) - } - result := gui.git.Rebase.GenericMergeOrRebaseAction(commandType, command) - if err := gui.checkMergeOrRebase(result); err != nil { - return err - } - return nil -} - -var conflictStrings = []string{ - "Failed to merge in the changes", - "When you have resolved this problem", - "fix conflicts", - "Resolve all conflicts manually", -} - -func isMergeConflictErr(errStr string) bool { - for _, str := range conflictStrings { - if strings.Contains(errStr, str) { - return true - } - } - - return false -} - -func (gui *Gui) checkMergeOrRebase(result error) error { - if err := gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil { - return err - } - if result == nil { - return nil - } else if strings.Contains(result.Error(), "No changes - did you forget to use") { - return gui.genericMergeCommand(REBASE_OPTION_SKIP) - } else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") { - return gui.genericMergeCommand(REBASE_OPTION_CONTINUE) - } else if strings.Contains(result.Error(), "No rebase in progress?") { - // assume in this case that we're already done - return nil - } else if isMergeConflictErr(result.Error()) { - return gui.c.Ask(types.AskOpts{ - Title: gui.c.Tr.FoundConflictsTitle, - Prompt: gui.c.Tr.FoundConflicts, - HandlersManageFocus: true, - HandleConfirm: func() error { - return gui.c.PushContext(gui.State.Contexts.Files) - }, - HandleClose: func() error { - if err := gui.returnFromContext(); err != nil { - return err - } - - return gui.genericMergeCommand(REBASE_OPTION_ABORT) - }, - }) - } else { - return gui.c.ErrorMsg(result.Error()) - } -} - -func (gui *Gui) abortMergeOrRebaseWithConfirm() error { - // prompt user to confirm that they want to abort, then do it - mode := gui.workingTreeStateNoun() - return gui.c.Ask(types.AskOpts{ - Title: fmt.Sprintf(gui.c.Tr.AbortTitle, mode), - Prompt: fmt.Sprintf(gui.c.Tr.AbortPrompt, mode), - HandleConfirm: func() error { - return gui.genericMergeCommand(REBASE_OPTION_ABORT) - }, - }) -} - -func (gui *Gui) workingTreeStateNoun() string { - workingTreeState := gui.git.Status.WorkingTreeState() - switch workingTreeState { - case enums.REBASE_MODE_NONE: - return "" - case enums.REBASE_MODE_MERGING: - return "merge" - default: - return "rebase" - } -} diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go index 39292c7ba..a460cc2bd 100644 --- a/pkg/gui/reflog_panel.go +++ b/pkg/gui/reflog_panel.go @@ -79,3 +79,22 @@ func (gui *Gui) handleViewReflogCommitFiles() error { WindowName: "commits", }) } + +func (gui *Gui) handleCopyReflogCommit() error { + commit := gui.getSelectedReflogCommit() + if commit == nil { + return nil + } + + return gui.helpers.cherryPick.Copy(commit, gui.State.FilteredReflogCommits, gui.State.Contexts.ReflogCommits) +} + +func (gui *Gui) handleCopyReflogCommitRange() error { + // just doing this to ensure something is selected + commit := gui.getSelectedReflogCommit() + if commit == nil { + return nil + } + + return gui.helpers.cherryPick.CopyRange(gui.State.Contexts.ReflogCommits.GetPanelState().GetSelectedLineIdx(), gui.State.FilteredReflogCommits, gui.State.Contexts.ReflogCommits) +} diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go index 2f08c57c5..62959d3e3 100644 --- a/pkg/gui/refresh.go +++ b/pkg/gui/refresh.go @@ -408,7 +408,7 @@ func (gui *Gui) refreshStateFiles() error { } if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { - gui.OnUIThread(func() error { return gui.promptToContinueRebase() }) + gui.OnUIThread(func() error { return gui.helpers.rebase.PromptToContinueRebase() }) } fileTreeViewModel.RWMutex.Lock() diff --git a/pkg/gui/ref_helper.go b/pkg/gui/refs_helper.go similarity index 76% rename from pkg/gui/ref_helper.go rename to pkg/gui/refs_helper.go index 1d1e408c2..f732ce204 100644 --- a/pkg/gui/ref_helper.go +++ b/pkg/gui/refs_helper.go @@ -6,14 +6,17 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" ) type RefsHelper struct { - c *types.ControllerCommon - git *commands.GitCommand + c *types.ControllerCommon + git *commands.GitCommand + getContexts func() context.ContextTree getState func() *GuiRepoState } @@ -21,12 +24,14 @@ type RefsHelper struct { func NewRefsHelper( c *types.ControllerCommon, git *commands.GitCommand, + getContexts func() context.ContextTree, getState func() *GuiRepoState, ) *RefsHelper { return &RefsHelper{ - c: c, - git: git, - getState: getState, + c: c, + git: git, + getContexts: getContexts, + getState: getState, } } @@ -134,3 +139,34 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error { Items: menuItems, }) } + +func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggestedBranchName string) error { + message := utils.ResolvePlaceholderString( + self.c.Tr.NewBranchNameBranchOff, + map[string]string{ + "branchName": fromFormattedName, + }, + ) + + return self.c.Prompt(types.PromptOpts{ + Title: message, + InitialContent: suggestedBranchName, + HandleConfirm: func(response string) error { + self.c.LogAction(self.c.Tr.Actions.CreateBranch) + if err := self.git.Branch.New(sanitizedBranchName(response), from); err != nil { + return err + } + + if self.c.CurrentContext() != self.getContexts().Branches { + if err := self.c.PushContext(self.getContexts().Branches); err != nil { + return err + } + } + + self.getContexts().BranchCommits.GetPanelState().SetSelectedLineIdx(0) + self.getContexts().Branches.GetPanelState().SetSelectedLineIdx(0) + + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }) +} diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go index 4164662ba..a611eb5c4 100644 --- a/pkg/gui/remote_branches_panel.go +++ b/pkg/gui/remote_branches_panel.go @@ -2,6 +2,7 @@ package gui import ( "fmt" + "strings" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -118,3 +119,15 @@ func (gui *Gui) handleEnterRemoteBranch() error { return gui.switchToSubCommitsContext(selectedBranch.RefName()) } + +func (gui *Gui) handleNewBranchOffRemoteBranch() error { + selectedBranch := gui.getSelectedRemoteBranch() + if selectedBranch == nil { + return nil + } + + // will set to the remote's branch name without the remote name + nameSuggestion := strings.SplitAfterN(selectedBranch.RefName(), "/", 2)[1] + + return gui.helpers.refs.NewBranch(selectedBranch.RefName(), selectedBranch.RefName(), nameSuggestion) +} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index d1c206587..9b7b03bb5 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -136,3 +136,12 @@ func (gui *Gui) handleViewStashFiles() error { WindowName: "stash", }) } + +func (gui *Gui) handleNewBranchOffStashEntry() error { + stashEntry := gui.getSelectedStashEntry() + if stashEntry == nil { + return nil + } + + return gui.helpers.refs.NewBranch(stashEntry.RefName(), stashEntry.Description(), "") +} diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 366d55441..40a7f92b7 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -48,7 +48,7 @@ func (gui *Gui) handleStatusClick() error { case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState)) if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { - return gui.handleCreateRebaseOptionsMenu() + return gui.helpers.rebase.CreateRebaseOptionsMenu() } if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { return gui.handleCreateRecentReposMenu() diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go index a6fae60c9..5d81d5a9c 100644 --- a/pkg/gui/sub_commits_panel.go +++ b/pkg/gui/sub_commits_panel.go @@ -102,3 +102,31 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error { return gui.c.PushContext(gui.State.Contexts.SubCommits) } + +func (gui *Gui) handleNewBranchOffSubCommit() error { + commit := gui.getSelectedSubCommit() + if commit == nil { + return nil + } + + return gui.helpers.refs.NewBranch(commit.RefName(), commit.Description(), "") +} + +func (gui *Gui) handleCopySubCommit() error { + commit := gui.getSelectedSubCommit() + if commit == nil { + return nil + } + + return gui.helpers.cherryPick.Copy(commit, gui.State.SubCommits, gui.State.Contexts.SubCommits) +} + +func (gui *Gui) handleCopySubCommitRange() error { + // just doing this to ensure something is selected + commit := gui.getSelectedSubCommit() + if commit == nil { + return nil + } + + return gui.helpers.cherryPick.CopyRange(gui.State.Contexts.SubCommits.GetPanelState().GetSelectedLineIdx(), gui.State.SubCommits, gui.State.Contexts.SubCommits) +} diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 9289c4f2d..4dbb0eca3 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -6,8 +6,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" ) -// if Go let me do private struct embedding of structs with public fields (which it should) -// I would just do that. But alas. type ControllerCommon struct { *common.Common IGuiCommon diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index ffdcb49a7..fcbadfd22 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -61,8 +61,8 @@ type IController interface { type IListContext interface { HasKeybindings - GetSelectedItem() (ListItem, bool) + GetSelectedItemId() string HandlePrevLine() error HandleNextLine() error HandleScrollLeft() error