diff --git a/pkg/app/daemon/daemon.go b/pkg/app/daemon/daemon.go index 95fa6bc9e..70490aef0 100644 --- a/pkg/app/daemon/daemon.go +++ b/pkg/app/daemon/daemon.go @@ -34,8 +34,8 @@ const ( DaemonKindExitImmediately DaemonKindCherryPick - DaemonKindMoveTodoUp - DaemonKindMoveTodoDown + DaemonKindMoveTodosUp + DaemonKindMoveTodosDown DaemonKindInsertBreak DaemonKindChangeTodoActions DaemonKindMoveFixupCommitDown @@ -56,8 +56,8 @@ func getInstruction() Instruction { DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction], DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction], DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction], - DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction], - DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction], + DaemonKindMoveTodosUp: deserializeInstruction[*MoveTodosUpInstruction], + DaemonKindMoveTodosDown: deserializeInstruction[*MoveTodosDownInstruction], DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction], } @@ -208,13 +208,15 @@ func (self *ChangeTodoActionsInstruction) SerializedInstructions() string { func (self *ChangeTodoActionsInstruction) run(common *common.Common) error { return handleInteractiveRebase(common, func(path string) error { - for _, c := range self.Changes { - if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction, getCommentChar()); err != nil { - return err + changes := lo.Map(self.Changes, func(c ChangeTodoAction, _ int) utils.TodoChange { + return utils.TodoChange{ + Sha: c.Sha, + OldAction: todo.Pick, + NewAction: c.NewAction, } - } + }) - return nil + return utils.EditRebaseTodo(path, changes, getCommentChar()) }) } @@ -247,51 +249,65 @@ func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error { }) } -type MoveTodoUpInstruction struct { - Sha string +type MoveTodosUpInstruction struct { + Shas []string } -func NewMoveTodoUpInstruction(sha string) Instruction { - return &MoveTodoUpInstruction{ - Sha: sha, +func NewMoveTodosUpInstruction(shas []string) Instruction { + return &MoveTodosUpInstruction{ + Shas: shas, } } -func (self *MoveTodoUpInstruction) Kind() DaemonKind { - return DaemonKindMoveTodoUp +func (self *MoveTodosUpInstruction) Kind() DaemonKind { + return DaemonKindMoveTodosUp } -func (self *MoveTodoUpInstruction) SerializedInstructions() string { +func (self *MoveTodosUpInstruction) SerializedInstructions() string { return serializeInstruction(self) } -func (self *MoveTodoUpInstruction) run(common *common.Common) error { +func (self *MoveTodosUpInstruction) run(common *common.Common) error { + todosToMove := lo.Map(self.Shas, func(sha string, _ int) utils.Todo { + return utils.Todo{ + Sha: sha, + Action: todo.Pick, + } + }) + return handleInteractiveRebase(common, func(path string) error { - return utils.MoveTodoUp(path, self.Sha, todo.Pick, getCommentChar()) + return utils.MoveTodosUp(path, todosToMove, getCommentChar()) }) } -type MoveTodoDownInstruction struct { - Sha string +type MoveTodosDownInstruction struct { + Shas []string } -func NewMoveTodoDownInstruction(sha string) Instruction { - return &MoveTodoDownInstruction{ - Sha: sha, +func NewMoveTodosDownInstruction(shas []string) Instruction { + return &MoveTodosDownInstruction{ + Shas: shas, } } -func (self *MoveTodoDownInstruction) Kind() DaemonKind { - return DaemonKindMoveTodoDown +func (self *MoveTodosDownInstruction) Kind() DaemonKind { + return DaemonKindMoveTodosDown } -func (self *MoveTodoDownInstruction) SerializedInstructions() string { +func (self *MoveTodosDownInstruction) SerializedInstructions() string { return serializeInstruction(self) } -func (self *MoveTodoDownInstruction) run(common *common.Common) error { +func (self *MoveTodosDownInstruction) run(common *common.Common) error { + todosToMove := lo.Map(self.Shas, func(sha string, _ int) utils.Todo { + return utils.Todo{ + Sha: sha, + Action: todo.Pick, + } + }) + return handleInteractiveRebase(common, func(path string) error { - return utils.MoveTodoDown(path, self.Sha, todo.Pick, getCommentChar()) + return utils.MoveTodosDown(path, todosToMove, getCommentChar()) }) } diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index fde049cda..03d4030ac 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -105,58 +105,49 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f return self.ContinueRebase() } -func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error { - baseShaOrRoot := getBaseShaOrRoot(commits, index+2) +func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx int, endIdx int) error { + baseShaOrRoot := getBaseShaOrRoot(commits, endIdx+2) - sha := commits[index].Sha - - msg := utils.ResolvePlaceholderString( - self.Tr.Log.MoveCommitDown, - map[string]string{ - "shortSha": utils.ShortSha(sha), - }, - ) - self.os.LogCommand(msg, false) + shas := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string { + return commit.Sha + }) return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ baseShaOrRoot: baseShaOrRoot, - instruction: daemon.NewMoveTodoDownInstruction(sha), + instruction: daemon.NewMoveTodosDownInstruction(shas), overrideEditor: true, }).Run() } -func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error { - baseShaOrRoot := getBaseShaOrRoot(commits, index+1) +func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int, endIdx int) error { + baseShaOrRoot := getBaseShaOrRoot(commits, endIdx+1) - sha := commits[index].Sha - - msg := utils.ResolvePlaceholderString( - self.Tr.Log.MoveCommitUp, - map[string]string{ - "shortSha": utils.ShortSha(sha), - }, - ) - self.os.LogCommand(msg, false) + shas := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string { + return commit.Sha + }) return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ baseShaOrRoot: baseShaOrRoot, - instruction: daemon.NewMoveTodoUpInstruction(sha), + instruction: daemon.NewMoveTodosUpInstruction(shas), overrideEditor: true, }).Run() } -func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error { - baseIndex := index + 1 +func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx int, endIdx int, action todo.TodoCommand) error { + baseIndex := endIdx + 1 if action == todo.Squash || action == todo.Fixup { baseIndex++ } baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex) - changes := []daemon.ChangeTodoAction{{ - Sha: commits[index].Sha, - NewAction: action, - }} + changes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) daemon.ChangeTodoAction { + return daemon.ChangeTodoAction{ + Sha: commit.Sha, + NewAction: action, + } + }) + self.os.LogCommand(logTodoChanges(changes), false) return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ @@ -200,7 +191,7 @@ func logTodoChanges(changes []daemon.ChangeTodoAction) string { changeTodoStr := strings.Join(lo.Map(changes, func(c daemon.ChangeTodoAction, _ int) string { return fmt.Sprintf("%s:%s", c.Sha, c.NewAction) }), "\n") - return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr) + return fmt.Sprintf("Changing TODO actions:\n%s", changeTodoStr) } type PrepareInteractiveRebaseCommandOpts struct { @@ -281,22 +272,45 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e }).Run() } -// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file -func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error { +// Sets the action for the given commits in the git-rebase-todo file +func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo.TodoCommand) error { + commitsWithAction := lo.Map(commits, func(commit *models.Commit, _ int) utils.TodoChange { + return utils.TodoChange{ + Sha: commit.Sha, + OldAction: commit.Action, + NewAction: action, + } + }) + return utils.EditRebaseTodo( - filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action, self.config.GetCoreCommentChar()) + filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), + commitsWithAction, + self.config.GetCoreCommentChar(), + ) } -// MoveTodoDown moves a rebase todo item down by one position -func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error { +func (self *RebaseCommands) MoveTodosDown(commits []*models.Commit) error { fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") - return utils.MoveTodoDown(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) + todosToMove := lo.Map(commits, func(commit *models.Commit, _ int) utils.Todo { + return utils.Todo{ + Sha: commit.Sha, + Action: commit.Action, + } + }) + + return utils.MoveTodosDown(fileName, todosToMove, self.config.GetCoreCommentChar()) } -// MoveTodoDown moves a rebase todo item down by one position -func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error { +func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error { fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") - return utils.MoveTodoUp(fileName, commit.Sha, commit.Action, self.config.GetCoreCommentChar()) + todosToMove := lo.Map(commits, func(commit *models.Commit, _ int) utils.Todo { + return utils.Todo{ + Sha: commit.Sha, + Action: commit.Action, + } + }) + + return utils.MoveTodosUp(fileName, todosToMove, self.config.GetCoreCommentChar()) } // SquashAllAboveFixupCommits squashes all fixup! commits above the given one diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 2b94dfa7a..1fe6738e3 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -2,6 +2,8 @@ package helpers import ( "fmt" + "os" + "path/filepath" "strings" "github.com/jesseduffield/gocui" @@ -80,6 +82,19 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error { } self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command)) + if status == enums.REBASE_MODE_REBASING { + todoFile, err := os.ReadFile( + filepath.Join(self.c.Git().RepoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo"), + ) + + if err != nil { + if !os.IsNotExist(err) { + return err + } + } else { + self.c.LogCommand(string(todoFile), false) + } + } commandType := "" switch status { diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 5fe08b85e..e1d20b554 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -45,6 +45,7 @@ func NewLocalCommitsController( c, c.Contexts().LocalCommits, c.Contexts().LocalCommits.GetSelected, + c.Contexts().LocalCommits.GetSelectedItems, ), } } @@ -55,17 +56,23 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ outsideFilterModeBindings := []*types.Binding{ { Key: opts.GetKey(opts.Config.Commits.SquashDown), - Handler: self.withItem(self.squashDown), + Handler: self.withItemsRange(self.squashDown), GetDisabledReason: self.require( - self.singleItemSelected(self.getDisabledReasonForSquashDown), + self.itemRangeSelected( + self.midRebaseCommandEnabled, + self.canSquashOrFixup, + ), ), Description: self.c.Tr.SquashDown, }, { Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), - Handler: self.withItem(self.fixup), + Handler: self.withItemsRange(self.fixup), GetDisabledReason: self.require( - self.singleItemSelected(self.getDisabledReasonForFixup), + self.itemRangeSelected( + self.midRebaseCommandEnabled, + self.canSquashOrFixup, + ), ), Description: self.c.Tr.FixupCommit, }, @@ -73,7 +80,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Key: opts.GetKey(opts.Config.Commits.RenameCommit), Handler: self.withItem(self.reword), GetDisabledReason: self.require( - self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), + self.singleItemSelected(self.rewordEnabled), ), Description: self.c.Tr.RewordCommit, }, @@ -81,23 +88,26 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), Handler: self.withItem(self.rewordEditor), GetDisabledReason: self.require( - self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), + self.singleItemSelected(self.rewordEnabled), ), Description: self.c.Tr.RenameCommitEditor, }, { Key: opts.GetKey(opts.Config.Universal.Remove), - Handler: self.withItem(self.drop), + Handler: self.withItemsRange(self.drop), GetDisabledReason: self.require( - self.singleItemSelected(self.rebaseCommandEnabled(todo.Drop)), + self.itemRangeSelected( + self.midRebaseCommandEnabled, + ), ), Description: self.c.Tr.DeleteCommit, }, { Key: opts.GetKey(editCommitKey), - Handler: self.withItem(self.edit), + Handler: self.withItems(self.edit), + // TODO: have disabled reason ensure that if we're not rebasing, we only select one commit GetDisabledReason: self.require( - self.singleItemSelected(self.rebaseCommandEnabled(todo.Edit)), + self.itemRangeSelected(self.midRebaseCommandEnabled), ), Description: self.c.Tr.EditCommit, }, @@ -107,7 +117,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ // when you manually select the base commit. Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase), Handler: self.withItem(self.quickStartInteractiveRebase), - GetDisabledReason: self.require(self.notMidRebase, self.canFindCommitForQuickStart), + GetDisabledReason: self.require(self.notMidRebase(self.c.Tr.AlreadyRebasing), self.canFindCommitForQuickStart), Description: self.c.Tr.QuickStartInteractiveRebase, Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{ "editKey": keybindings.Label(editCommitKey), @@ -115,9 +125,9 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Key: opts.GetKey(opts.Config.Commits.PickCommit), - Handler: self.withItem(self.pick), + Handler: self.withItems(self.pick), GetDisabledReason: self.require( - self.singleItemSelected(self.rebaseCommandEnabled(todo.Pick)), + self.itemRangeSelected(self.pickEnabled), ), Description: self.c.Tr.PickCommit, }, @@ -131,22 +141,28 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), Handler: self.withItem(self.squashAllAboveFixupCommits), GetDisabledReason: self.require( - self.notMidRebase, + self.notMidRebase(self.c.Tr.AlreadyRebasing), self.singleItemSelected(), ), Description: self.c.Tr.SquashAboveCommits, }, { - Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), - Handler: self.withItem(self.moveDown), - GetDisabledReason: self.require(self.singleItemSelected()), - Description: self.c.Tr.MoveDownCommit, + Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), + Handler: self.withItemsRange(self.moveDown), + GetDisabledReason: self.require(self.itemRangeSelected( + self.midRebaseCommandEnabled, + self.canMoveDown, + )), + Description: self.c.Tr.MoveDownCommit, }, { - Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), - Handler: self.withItem(self.moveUp), - GetDisabledReason: self.require(self.singleItemSelected()), - Description: self.c.Tr.MoveUpCommit, + Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), + Handler: self.withItemsRange(self.moveUp), + GetDisabledReason: self.require(self.itemRangeSelected( + self.midRebaseCommandEnabled, + self.canMoveUp, + )), + Description: self.c.Tr.MoveUpCommit, }, { Key: opts.GetKey(opts.Config.Commits.PasteCommits), @@ -263,13 +279,9 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts { return nil } -func (self *LocalCommitsController) squashDown(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Squash, commit) - if err != nil { - return err - } - if applied { - return nil +func (self *LocalCommitsController) squashDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error { + if self.isRebasing() { + return self.updateTodos(todo.Squash, selectedCommits) } return self.c.Confirm(types.ConfirmOpts{ @@ -278,27 +290,15 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.SquashCommitDown) - return self.interactiveRebase(todo.Squash) + return self.interactiveRebase(todo.Squash, startIdx, endIdx) }) }, }) } -func (self *LocalCommitsController) getDisabledReasonForSquashDown(commit *models.Commit) *types.DisabledReason { - if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { - return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} - } - - return self.rebaseCommandEnabled(todo.Squash)(commit) -} - -func (self *LocalCommitsController) fixup(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Fixup, commit) - if err != nil { - return err - } - if applied { - return nil +func (self *LocalCommitsController) fixup(selectedCommits []*models.Commit, startIdx int, endIdx int) error { + if self.isRebasing() { + return self.updateTodos(todo.Fixup, selectedCommits) } return self.c.Confirm(types.ConfirmOpts{ @@ -307,29 +307,13 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.FixupCommit) - return self.interactiveRebase(todo.Fixup) + return self.interactiveRebase(todo.Fixup, startIdx, endIdx) }) }, }) } -func (self *LocalCommitsController) getDisabledReasonForFixup(commit *models.Commit) *types.DisabledReason { - if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { - return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} - } - - return self.rebaseCommandEnabled(todo.Squash)(commit) -} - func (self *LocalCommitsController) reword(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Reword, commit) - if err != nil { - return err - } - if applied { - return nil - } - commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Sha) if err != nil { return self.c.Error(err) @@ -404,14 +388,6 @@ func (self *LocalCommitsController) doRewordEditor() error { } func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error { - midRebase, err := self.handleMidRebaseCommand(todo.Reword, commit) - if err != nil { - return err - } - if midRebase { - return nil - } - if self.c.UserConfig.Gui.SkipRewordInEditorWarning { return self.doRewordEditor() } else { @@ -423,37 +399,37 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error { } } -func (self *LocalCommitsController) drop(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Drop, commit) - if err != nil { - return err - } - if applied { - return nil +func (self *LocalCommitsController) drop(selectedCommits []*models.Commit, startIdx int, endIdx int) error { + if self.isRebasing() { + return self.updateTodos(todo.Drop, selectedCommits) } return self.c.Confirm(types.ConfirmOpts{ - Title: self.c.Tr.DeleteCommitTitle, - Prompt: self.c.Tr.DeleteCommitPrompt, + Title: self.c.Tr.DropCommitTitle, + Prompt: self.c.Tr.DropCommitPrompt, HandleConfirm: func() error { - return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error { + return self.c.WithWaitingStatus(self.c.Tr.DroppingStatus, func(gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.DropCommit) - return self.interactiveRebase(todo.Drop) + return self.interactiveRebase(todo.Drop, startIdx, endIdx) }) }, }) } -func (self *LocalCommitsController) edit(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Edit, commit) - if err != nil { - return err - } - if applied { - return nil +func (self *LocalCommitsController) edit(selectedCommits []*models.Commit) error { + if self.isRebasing() { + return self.updateTodos(todo.Edit, selectedCommits) } - return self.startInteractiveRebaseWithEdit(commit, commit) + // TODO: support range select here (start a rebase and set the selected commits + // to 'edit' in the todo file) + if len(selectedCommits) > 1 { + return self.c.ErrorMsg(self.c.Tr.RangeSelectNotSupported) + } + + selectedCommit := selectedCommits[0] + + return self.startInteractiveRebaseWithEdit(selectedCommit, selectedCommit) } func (self *LocalCommitsController) quickStartInteractiveRebase(selectedCommit *models.Commit) error { @@ -504,13 +480,9 @@ func (self *LocalCommitsController) findCommitForQuickStartInteractiveRebase() ( return commit, nil } -func (self *LocalCommitsController) pick(commit *models.Commit) error { - applied, err := self.handleMidRebaseCommand(todo.Pick, commit) - if err != nil { - return err - } - if applied { - return nil +func (self *LocalCommitsController) pick(selectedCommits []*models.Commit) error { + if self.isRebasing() { + return self.updateTodos(todo.Pick, selectedCommits) } // at this point we aren't actually rebasing so we will interpret this as an @@ -518,159 +490,93 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error { return self.pullFiles() } -func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error { - err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action) +func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, startIdx int, endIdx int) error { + // When performing an action that will remove the selected commits, we need to select the + // next commit down (which will end up at the start index after the action is performed) + if action == todo.Drop || action == todo.Fixup || action == todo.Squash { + self.context().SetSelection(startIdx) + } + + err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, startIdx, endIdx, action) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) } -// handleMidRebaseCommand sees if the selected commit is in fact a rebasing +// updateTodos sees if the selected commit is in fact a rebasing // commit meaning you are trying to edit the todo file rather than actually // begin a rebase. It then updates the todo file with that action -func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { - if !commit.IsTODO() { - return false, nil +func (self *LocalCommitsController) updateTodos(action todo.TodoCommand, selectedCommits []*models.Commit) error { + if err := self.c.Git().Rebase.EditRebaseTodo(selectedCommits, action); err != nil { + return self.c.Error(err) } - self.c.LogAction("Update rebase TODO") - - msg := utils.ResolvePlaceholderString( - self.c.Tr.Log.HandleMidRebaseCommand, - map[string]string{ - "shortSha": commit.ShortSha(), - "action": action.String(), - }, - ) - self.c.LogCommand(msg, false) - - if err := self.c.Git().Rebase.EditRebaseTodo(commit, action); err != nil { - return false, self.c.Error(err) - } - - return true, self.c.Refresh(types.RefreshOptions{ + return self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, }) } -func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand) func(*models.Commit) *types.DisabledReason { - return func(commit *models.Commit) *types.DisabledReason { - if commit.Action == models.ActionConflict { - return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} - } - - if !commit.IsTODO() { - if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - // If we are in a rebase, the only action that is allowed for - // non-todo commits is rewording the current head commit - if !(action == todo.Reword && self.isHeadCommit()) { - return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} - } - } - - return nil - } - - // for now we do not support setting 'reword' because it requires an editor - // and that means we either unconditionally wait around for the subprocess to ask for - // our input or we set a lazygit client as the EDITOR env variable and have it - // request us to edit the commit message when prompted. - if action == todo.Reword { - return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported} - } - - if allowed := isChangeOfRebaseTodoAllowed(action); !allowed { - return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} - } - - return nil +func (self *LocalCommitsController) rewordEnabled(commit *models.Commit) *types.DisabledReason { + // for now we do not support setting 'reword' on TODO commits because it requires an editor + // and that means we either unconditionally wait around for the subprocess to ask for + // our input or we set a lazygit client as the EDITOR env variable and have it + // request us to edit the commit message when prompted. + if commit.IsTODO() { + return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported} } + + // If we are in a rebase, the only action that is allowed for + // non-todo commits is rewording the current head commit + if self.isRebasing() && !self.isHeadCommit() { + return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} + } + + return nil } -func (self *LocalCommitsController) moveDown(commit *models.Commit) error { - index := self.context().GetSelectedLineIdx() - commits := self.c.Model().Commits +func (self *LocalCommitsController) isRebasing() bool { + return self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE +} - // can't move past the initial commit - if index >= len(commits)-1 { - return nil - } - - if commit.IsTODO() { - if !commits[index+1].IsTODO() || commits[index+1].Action == models.ActionConflict { - return nil - } - - // logging directly here because MoveTodoDown doesn't have enough information - // to provide a useful log - self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) - - msg := utils.ResolvePlaceholderString( - self.c.Tr.Log.MovingCommitDown, - map[string]string{ - "shortSha": commit.ShortSha(), - }, - ) - self.c.LogCommand(msg, false) - - if err := self.c.Git().Rebase.MoveTodoDown(commit); err != nil { +func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error { + if self.isRebasing() { + if err := self.c.Git().Rebase.MoveTodosDown(selectedCommits); err != nil { return self.c.Error(err) } - self.context().MoveSelectedLine(1) + self.context().MoveSelection(1) + return self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, }) } - if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { - return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) - } - return self.c.WithWaitingStatusSync(self.c.Tr.MovingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) - err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index) + err := self.c.Git().Rebase.MoveCommitsDown(self.c.Model().Commits, startIdx, endIdx) if err == nil { - self.context().MoveSelectedLine(1) + self.context().MoveSelection(1) } return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{Mode: types.SYNC}) }) } -func (self *LocalCommitsController) moveUp(commit *models.Commit) error { - index := self.context().GetSelectedLineIdx() - if index == 0 { - return nil - } - - if commit.IsTODO() { - // logging directly here because MoveTodoDown doesn't have enough information - // to provide a useful log - self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) - msg := utils.ResolvePlaceholderString( - self.c.Tr.Log.MovingCommitUp, - map[string]string{ - "shortSha": commit.ShortSha(), - }, - ) - self.c.LogCommand(msg, false) - - if err := self.c.Git().Rebase.MoveTodoUp(self.c.Model().Commits[index]); err != nil { +func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, startIdx int, endIdx int) error { + if self.isRebasing() { + if err := self.c.Git().Rebase.MoveTodosUp(selectedCommits); err != nil { return self.c.Error(err) } - self.context().MoveSelectedLine(-1) + self.context().MoveSelection(-1) + return self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, }) } - if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { - return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) - } - return self.c.WithWaitingStatusSync(self.c.Tr.MovingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) - err := self.c.Git().Rebase.MoveCommitUp(self.c.Model().Commits, index) + err := self.c.Git().Rebase.MoveCommitsUp(self.c.Model().Commits, startIdx, endIdx) if err == nil { - self.context().MoveSelectedLine(-1) + self.context().MoveSelection(-1) } return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{Mode: types.SYNC}) @@ -693,10 +599,6 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { }) } - if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { - return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) - } - return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.AmendCommitTitle, Prompt: self.c.Tr.AmendCommitPrompt, @@ -713,7 +615,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { } func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.DisabledReason { - if !self.isHeadCommit() && self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { + if !self.isHeadCommit() && self.isRebasing() { return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} } @@ -721,10 +623,6 @@ func (self *LocalCommitsController) canAmend(commit *models.Commit) *types.Disab } func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { - if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() { - return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) - } - return self.c.Menu(types.CreateMenuOptions{ Title: "Amend commit attribute", Items: []*types.MenuItem{ @@ -846,7 +744,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C } func (self *LocalCommitsController) afterRevertCommit() error { - self.context().MoveSelectedLine(1) + self.context().MoveSelection(1) return self.c.Refresh(types.RefreshOptions{ Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES}, }) @@ -895,24 +793,6 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co }) } -// For getting disabled reason -func (self *LocalCommitsController) notMidRebase() *types.DisabledReason { - if self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE { - return &types.DisabledReason{Text: self.c.Tr.AlreadyRebasing} - } - - return nil -} - -// For getting disabled reason -func (self *LocalCommitsController) canFindCommitForQuickStart() *types.DisabledReason { - if _, err := self.findCommitForQuickStartInteractiveRebase(); err != nil { - return &types.DisabledReason{Text: err.Error(), ShowErrorInPanel: true} - } - - return nil -} - func (self *LocalCommitsController) createTag(commit *models.Commit) error { return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {}) } @@ -1079,15 +959,88 @@ func (self *LocalCommitsController) isHeadCommit() bool { return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx()) } -func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool { - allowedActions := []todo.TodoCommand{ - todo.Pick, - todo.Drop, - todo.Edit, - todo.Fixup, - todo.Squash, - todo.Reword, +func (self *LocalCommitsController) notMidRebase(message string) func() *types.DisabledReason { + return func() *types.DisabledReason { + if self.isRebasing() { + return &types.DisabledReason{Text: message} + } + + return nil + } +} + +func (self *LocalCommitsController) canFindCommitForQuickStart() *types.DisabledReason { + if _, err := self.findCommitForQuickStartInteractiveRebase(); err != nil { + return &types.DisabledReason{Text: err.Error(), ShowErrorInPanel: true} } - return lo.Contains(allowedActions, action) + return nil +} + +func (self *LocalCommitsController) canSquashOrFixup(_selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { + if endIdx >= len(self.c.Model().Commits)-1 { + return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} + } + + return nil +} + +func (self *LocalCommitsController) canMoveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { + if endIdx >= len(self.c.Model().Commits)-1 { + return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} + } + + if self.isRebasing() { + commits := self.c.Model().Commits + + if !commits[endIdx+1].IsTODO() || commits[endIdx+1].Action == models.ActionConflict { + return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} + } + } + + return nil +} + +func (self *LocalCommitsController) canMoveUp(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { + if startIdx == 0 { + return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} + } + + if self.isRebasing() { + commits := self.c.Model().Commits + + if !commits[startIdx-1].IsTODO() || commits[startIdx-1].Action == models.ActionConflict { + return &types.DisabledReason{Text: self.c.Tr.CannotMoveAnyFurther} + } + } + + return nil +} + +// Ensures that if we are mid-rebase, we're only selecting valid commits (non-conflict TODO commits) +func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { + if !self.isRebasing() { + return nil + } + + for _, commit := range selectedCommits { + if !commit.IsTODO() { + return &types.DisabledReason{Text: self.c.Tr.MustSelectTodoCommits} + } + + if commit.Action == models.ActionConflict { + return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} + } + } + + return nil +} + +func (self *LocalCommitsController) pickEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason { + if !self.isRebasing() { + // if not rebasing, we're going to do a pull so we don't care about the selection + return nil + } + + return self.midRebaseCommandEnabled(selectedCommits, startIdx, endIdx) } diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 8386bce1e..298353b88 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -217,8 +217,8 @@ func chineseTranslationSet() TranslationSet { ScrollDownMainPanel: "向下滚动主面板", AmendCommitTitle: "修改提交", AmendCommitPrompt: "您确定要使用暂存文件来修改此提交吗?", - DeleteCommitTitle: "删除提交", - DeleteCommitPrompt: "您确定要删除此提交吗?", + DropCommitTitle: "删除提交", + DropCommitPrompt: "您确定要删除此提交吗?", PullingStatus: "正在拉取", PushingStatus: "正在推送", FetchingStatus: "正在抓取", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 1e2eaa689..dc391d743 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -181,8 +181,8 @@ func dutchTranslationSet() TranslationSet { ScrollDownMainPanel: "Scroll naar beneden vanaf hoofdpaneel", AmendCommitTitle: "Commit wijzigen", AmendCommitPrompt: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?", - DeleteCommitTitle: "Verwijder commit", - DeleteCommitPrompt: "Weet je zeker dat je deze commit wil verwijderen?", + DropCommitTitle: "Verwijder commit", + DropCommitPrompt: "Weet je zeker dat je deze commit wil verwijderen?", PullingStatus: "Pullen", PushingStatus: "Pushen", FetchingStatus: "Fetchen", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 35e7dd687..c40e61241 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -119,6 +119,7 @@ type TranslationSet struct { DeleteCommit string MoveDownCommit string MoveUpCommit string + CannotMoveAnyFurther string EditCommit string AmendToCommit string ResetAuthor string @@ -239,6 +240,7 @@ type TranslationSet struct { SimpleRebase string InteractiveRebase string InteractiveRebaseTooltip string + MustSelectTodoCommits string ConfirmMerge string FwdNoUpstream string FwdNoLocalUpstream string @@ -270,14 +272,15 @@ type TranslationSet struct { ScrollDownMainPanel string AmendCommitTitle string AmendCommitPrompt string - DeleteCommitTitle string - DeleteCommitPrompt string + DropCommitTitle string + DropCommitPrompt string PullingStatus string PushingStatus string FetchingStatus string SquashingStatus string FixingStatus string DeletingStatus string + DroppingStatus string MovingStatus string RebasingStatus string MergingStatus string @@ -686,8 +689,6 @@ type Log struct { CherryPickCommits string HandleUndo string HandleMidRebaseCommand string - MovingCommitUp string - MovingCommitDown string RemoveFile string CopyToClipboard string Remove string @@ -945,8 +946,8 @@ func EnglishTranslationSet() TranslationSet { UpdateRefHere: "Update branch '{{.ref}}' here", CannotSquashOrFixupFirstCommit: "There's no commit below to squash into", Fixup: "Fixup", - SureFixupThisCommit: "Are you sure you want to 'fixup' this commit? It will be merged into the commit below", - SureSquashThisCommit: "Are you sure you want to squash this commit into the commit below?", + SureFixupThisCommit: "Are you sure you want to 'fixup' the selected commit(s) into the commit below?", + SureSquashThisCommit: "Are you sure you want to squash the selected commit(s) into the commit below?", Squash: "Squash", PickCommit: "Pick commit (when mid-rebase)", RevertCommit: "Revert commit", @@ -954,6 +955,7 @@ func EnglishTranslationSet() TranslationSet { DeleteCommit: "Delete commit", MoveDownCommit: "Move commit down one", MoveUpCommit: "Move commit up one", + CannotMoveAnyFurther: "Cannot move any further", EditCommit: "Edit commit", AmendToCommit: "Amend commit with staged changes", ResetAuthor: "Reset author", @@ -1079,6 +1081,7 @@ func EnglishTranslationSet() TranslationSet { SimpleRebase: "Simple rebase", InteractiveRebase: "Interactive rebase", InteractiveRebaseTooltip: "Begin an interactive rebase with a break at the start, so you can update the TODO commits before continuing", + MustSelectTodoCommits: "When rebasing, this action only works on a selection of TODO commits.", ConfirmMerge: "Are you sure you want to merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'?", FwdNoUpstream: "Cannot fast-forward a branch with no upstream", FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally", @@ -1110,14 +1113,15 @@ func EnglishTranslationSet() TranslationSet { ScrollDownMainPanel: "Scroll down main panel", AmendCommitTitle: "Amend commit", AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?", - DeleteCommitTitle: "Delete commit", - DeleteCommitPrompt: "Are you sure you want to delete this commit?", + DropCommitTitle: "Drop commit", + DropCommitPrompt: "Are you sure you want to drop the selected commit(s)?", PullingStatus: "Pulling", PushingStatus: "Pushing", FetchingStatus: "Fetching", SquashingStatus: "Squashing", FixingStatus: "Fixing up", DeletingStatus: "Deleting", + DroppingStatus: "Dropping", MovingStatus: "Moving", RebasingStatus: "Rebasing", MergingStatus: "Merging", @@ -1626,8 +1630,6 @@ func EnglishTranslationSet() TranslationSet { CherryPickCommits: "Cherry-picking commits:\n'{{.commitLines}}'", HandleUndo: "Undoing last conflict resolution", HandleMidRebaseCommand: "Updating rebase action of commit {{.shortSha}} to '{{.action}}'", - MovingCommitUp: "Moving commit {{.shortSha}} up", - MovingCommitDown: "Moving commit {{.shortSha}} down", RemoveFile: "Deleting path '{{.path}}'", CopyToClipboard: "Copying '{{.str}}' to clipboard", Remove: "Removing '{{.filename}}'", diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go index 3da17b097..864bd4aa3 100644 --- a/pkg/i18n/japanese.go +++ b/pkg/i18n/japanese.go @@ -221,8 +221,8 @@ func japaneseTranslationSet() TranslationSet { ScrollDownMainPanel: "メインパネルを下にスクロール", AmendCommitTitle: "Amendコミット", AmendCommitPrompt: "ステージされたファイルで現在のコミットをamendします。よろしいですか?", - DeleteCommitTitle: "コミットを削除", - DeleteCommitPrompt: "選択されたコミットを削除します。よろしいですか?", + DropCommitTitle: "コミットを削除", + DropCommitPrompt: "選択されたコミットを削除します。よろしいですか?", PullingStatus: "Pull中", PushingStatus: "Push中", FetchingStatus: "Fetch中", diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go index 3c4d0ceab..14a70e06d 100644 --- a/pkg/i18n/korean.go +++ b/pkg/i18n/korean.go @@ -218,8 +218,8 @@ func koreanTranslationSet() TranslationSet { ScrollDownMainPanel: "메인 패널을 아래로로 스크롤", AmendCommitTitle: "Amend commit", AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?", - DeleteCommitTitle: "커밋 삭제", - DeleteCommitPrompt: "정말로 선택한 커밋을 삭제하시겠습니까?", + DropCommitTitle: "커밋 삭제", + DropCommitPrompt: "정말로 선택한 커밋을 삭제하시겠습니까?", PullingStatus: "업데이트 중", PushingStatus: "푸시 중", FetchingStatus: "패치 중", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index e1515a948..373c7b771 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -147,8 +147,8 @@ func polishTranslationSet() TranslationSet { ScrollUp: "Przewiń w górę", AmendCommitTitle: "Popraw commit", AmendCommitPrompt: "Czy na pewno chcesz poprawić ten commit plikami z poczekalni?", - DeleteCommitTitle: "Usuń commit", - DeleteCommitPrompt: "Czy na pewno usunąć ten commit?", + DropCommitTitle: "Usuń commit", + DropCommitPrompt: "Czy na pewno usunąć ten commit?", PullingStatus: "Pobieranie zmian", PushingStatus: "Wysyłanie zmian", FetchingStatus: "Pobieram", diff --git a/pkg/i18n/russian.go b/pkg/i18n/russian.go index 1522a0f1c..dca93fd27 100644 --- a/pkg/i18n/russian.go +++ b/pkg/i18n/russian.go @@ -262,8 +262,8 @@ func RussianTranslationSet() TranslationSet { ScrollDownMainPanel: "Прокрутить вниз главную панель", AmendCommitTitle: "Править коммит (amend)", AmendCommitPrompt: "Вы уверены, что хотите править этот коммит проиндексированными файлами?", - DeleteCommitTitle: "Удалить коммит", - DeleteCommitPrompt: "Вы уверены, что хотите удалить этот коммит?", + DropCommitTitle: "Удалить коммит", + DropCommitPrompt: "Вы уверены, что хотите удалить этот коммит?", PullingStatus: "Получение и слияние изменении", PushingStatus: "Отправка изменении", FetchingStatus: "Получение изменении", diff --git a/pkg/i18n/traditional_chinese.go b/pkg/i18n/traditional_chinese.go index b9519bfcd..c2fe0bd5e 100644 --- a/pkg/i18n/traditional_chinese.go +++ b/pkg/i18n/traditional_chinese.go @@ -293,8 +293,8 @@ func traditionalChineseTranslationSet() TranslationSet { ScrollDownMainPanel: "向下捲動主面板", AmendCommitTitle: "修正提交", AmendCommitPrompt: "你確定要使用預存的檔案修正此提交嗎?", - DeleteCommitTitle: "刪除提交", - DeleteCommitPrompt: "你確定要刪除此提交嗎?", + DropCommitTitle: "刪除提交", + DropCommitPrompt: "你確定要刪除此提交嗎?", PullingStatus: "拉取", PushingStatus: "推送", FetchingStatus: "擷取", diff --git a/pkg/integration/tests/demo/undo.go b/pkg/integration/tests/demo/undo.go index 4b47e0726..830b1849f 100644 --- a/pkg/integration/tests/demo/undo.go +++ b/pkg/integration/tests/demo/undo.go @@ -22,8 +22,8 @@ var Undo = NewIntegrationTest(NewIntegrationTestArgs{ confirmCommitDrop := func() { t.ExpectPopup().Confirmation(). - Title(Equals("Delete commit")). - Content(Equals("Are you sure you want to delete this commit?")). + Title(Equals("Drop commit")). + Content(Equals("Are you sure you want to drop the selected commit(s)?")). Wait(500). Confirm() } diff --git a/pkg/integration/tests/interactive_rebase/drop_with_custom_comment_char.go b/pkg/integration/tests/interactive_rebase/drop_with_custom_comment_char.go index dbaa77b90..a6868e44f 100644 --- a/pkg/integration/tests/interactive_rebase/drop_with_custom_comment_char.go +++ b/pkg/integration/tests/interactive_rebase/drop_with_custom_comment_char.go @@ -23,8 +23,8 @@ var DropWithCustomCommentChar = NewIntegrationTest(NewIntegrationTestArgs{ Press(keys.Universal.Remove). Tap(func() { t.ExpectPopup().Confirmation(). - Title(Equals("Delete commit")). - Content(Equals("Are you sure you want to delete this commit?")). + Title(Equals("Drop commit")). + Content(Equals("Are you sure you want to drop the selected commit(s)?")). Confirm() }). Lines( diff --git a/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go b/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go index 78cb875ac..88417ccdd 100644 --- a/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go +++ b/pkg/integration/tests/interactive_rebase/edit_non_todo_commit_during_rebase.go @@ -29,6 +29,6 @@ var EditNonTodoCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("commit 01")). Press(keys.Universal.Edit) - t.ExpectToast(Contains("Can't perform this action during a rebase")) + t.ExpectToast(Contains("Disabled: When rebasing, this action only works on a selection of TODO commits.")) }, }) diff --git a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go index 85a3df27c..61b14fafd 100644 --- a/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go +++ b/pkg/integration/tests/interactive_rebase/edit_the_confl_commit.go @@ -39,6 +39,6 @@ var EditTheConflCommit = NewIntegrationTest(NewIntegrationTestArgs{ NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")). Press(keys.Commits.RenameCommit) - t.ExpectToast(Contains("Changing this kind of rebase todo entry is not allowed")) + t.ExpectToast(Contains("Disabled: Rewording commits while interactively rebasing is not currently supported")) }, }) diff --git a/pkg/integration/tests/interactive_rebase/fixup_second_commit.go b/pkg/integration/tests/interactive_rebase/fixup_second_commit.go index 57648035d..c5eec4a82 100644 --- a/pkg/integration/tests/interactive_rebase/fixup_second_commit.go +++ b/pkg/integration/tests/interactive_rebase/fixup_second_commit.go @@ -29,7 +29,7 @@ var FixupSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{ Tap(func() { t.ExpectPopup().Confirmation(). Title(Equals("Fixup")). - Content(Equals("Are you sure you want to 'fixup' this commit? It will be merged into the commit below")). + Content(Equals("Are you sure you want to 'fixup' the selected commit(s) into the commit below?")). Confirm() }). Lines( diff --git a/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go b/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go new file mode 100644 index 000000000..6dcb12ea6 --- /dev/null +++ b/pkg/integration/tests/interactive_rebase/mid_rebase_range_select.go @@ -0,0 +1,205 @@ +package interactive_rebase + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var MidRebaseRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Do various things with range selection in the commits view when mid-rebase", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + CreateNCommits(10) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + TopLines( + Contains("commit 10").IsSelected(), + ). + NavigateToLine(Contains("commit 07")). + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("commit 10"), + Contains("commit 09"), + Contains("commit 08"), + Contains("commit 07").IsSelected(), + Contains("commit 06").IsSelected(), + Contains("commit 05"), + Contains("commit 04"), + ). + // Verify we can't perform an edit on multiple commits (it's not supported + // yet) + Press(keys.Universal.Edit). + Tap(func() { + // This ought to be a toast but I'm too lazy to implement that right now. + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("Action does not support range selection, please select a single item")). + Confirm() + }). + NavigateToLine(Contains("commit 05")). + // Start a rebase + Press(keys.Universal.Edit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("pick").Contains("commit 07"), + Contains("pick").Contains("commit 06"), + Contains("<-- YOU ARE HERE --- commit 05").IsSelected(), + Contains("commit 04"), + ). + SelectPreviousItem(). + // perform various actions on a range of commits + Press(keys.Universal.RangeSelectUp). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("pick").Contains("commit 07").IsSelected(), + Contains("pick").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.MarkCommitAsFixup). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("fixup").Contains("commit 07").IsSelected(), + Contains("fixup").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.PickCommit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("pick").Contains("commit 07").IsSelected(), + Contains("pick").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Universal.Edit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("edit").Contains("commit 07").IsSelected(), + Contains("edit").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.SquashDown). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.MoveDownCommit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). + Press(keys.Commits.MoveUpCommit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("pick").Contains("commit 08"), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.MoveUpCommit). + TopLines( + Contains("pick").Contains("commit 10"), + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.MoveUpCommit). + TopLines( + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + Press(keys.Commits.MoveUpCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). + TopLines( + Contains("squash").Contains("commit 07").IsSelected(), + Contains("squash").Contains("commit 06").IsSelected(), + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08"), + Contains("<-- YOU ARE HERE --- commit 05"), + Contains("commit 04"), + ). + // Verify we can't perform an action on a range that includes both + // TODO and non-TODO commits + NavigateToLine(Contains("commit 08")). + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("squash").Contains("commit 07"), + Contains("squash").Contains("commit 06"), + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05").IsSelected(), + Contains("commit 04"), + ). + Press(keys.Commits.MarkCommitAsFixup). + Tap(func() { + t.ExpectToast(Contains("Disabled: When rebasing, this action only works on a selection of TODO commits.")) + }). + TopLines( + Contains("squash").Contains("commit 07"), + Contains("squash").Contains("commit 06"), + Contains("pick").Contains("commit 10"), + Contains("pick").Contains("commit 09"), + Contains("pick").Contains("commit 08").IsSelected(), + Contains("<-- YOU ARE HERE --- commit 05").IsSelected(), + Contains("commit 04"), + ). + // continue the rebase + Tap(func() { + t.Common().ContinueRebase() + }). + TopLines( + Contains("commit 10"), + Contains("commit 09"), + Contains("commit 08"), + Contains("commit 05"), + // selected indexes are retained, though we may want to clear it + // in future (not sure what the best behaviour is right now) + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/interactive_rebase/move.go b/pkg/integration/tests/interactive_rebase/move.go index 8eca1073f..3f1f23755 100644 --- a/pkg/integration/tests/interactive_rebase/move.go +++ b/pkg/integration/tests/interactive_rebase/move.go @@ -45,6 +45,9 @@ var Move = NewIntegrationTest(NewIntegrationTestArgs{ ). // assert nothing happens upon trying to move beyond the last commit Press(keys.Commits.MoveDownCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). Lines( Contains("commit 03"), Contains("commit 02"), @@ -74,6 +77,9 @@ var Move = NewIntegrationTest(NewIntegrationTestArgs{ ). // assert nothing happens upon trying to move beyond the first commit Press(keys.Commits.MoveUpCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). Lines( Contains("commit 04").IsSelected(), Contains("commit 03"), diff --git a/pkg/integration/tests/interactive_rebase/move_in_rebase.go b/pkg/integration/tests/interactive_rebase/move_in_rebase.go index adce14409..48b74a7a4 100644 --- a/pkg/integration/tests/interactive_rebase/move_in_rebase.go +++ b/pkg/integration/tests/interactive_rebase/move_in_rebase.go @@ -45,8 +45,11 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{ Contains("commit 03"), Contains("YOU ARE HERE").Contains("commit 01"), ). - Press(keys.Commits.MoveUpCommit). // assert we can't move past the top + Press(keys.Commits.MoveUpCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). Lines( Contains("commit 02").IsSelected(), Contains("commit 04"), @@ -69,6 +72,9 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{ ). // assert we can't move past the bottom Press(keys.Commits.MoveDownCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). Lines( Contains("commit 04"), Contains("commit 03"), diff --git a/pkg/integration/tests/interactive_rebase/outside_rebase_range_select.go b/pkg/integration/tests/interactive_rebase/outside_rebase_range_select.go new file mode 100644 index 000000000..d30ae0d64 --- /dev/null +++ b/pkg/integration/tests/interactive_rebase/outside_rebase_range_select.go @@ -0,0 +1,155 @@ +package interactive_rebase + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var OutsideRebaseRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Do various things with range selection in the commits view when outside rebase", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell. + CreateNCommits(10) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + TopLines( + Contains("commit 10").IsSelected(), + ). + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("commit 10").IsSelected(), + Contains("commit 09").IsSelected(), + Contains("commit 08"), + ). + // Drop commits + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Drop commit")). + Content(Contains("Are you sure you want to drop the selected commit(s)?")). + Confirm() + }). + TopLines( + Contains("commit 08").IsSelected(), + Contains("commit 07"), + ). + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("commit 08").IsSelected(), + Contains("commit 07").IsSelected(), + Contains("commit 06"), + ). + // Squash commits + Press(keys.Commits.SquashDown). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Squash")). + Content(Contains("Are you sure you want to squash the selected commit(s) into the commit below?")). + Confirm() + }). + TopLines( + Contains("commit 06").IsSelected(), + Contains("commit 05"), + Contains("commit 04"), + ). + // Verify commit messages are concatenated + Tap(func() { + t.Views().Main(). + ContainsLines( + Contains("commit 06"), + AnyString(), + Contains("commit 07"), + AnyString(), + Contains("commit 08"), + ) + }). + // Fixup commits + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("commit 06").IsSelected(), + Contains("commit 05").IsSelected(), + Contains("commit 04"), + ). + Press(keys.Commits.MarkCommitAsFixup). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Fixup")). + Content(Contains("Are you sure you want to 'fixup' the selected commit(s) into the commit below?")). + Confirm() + }). + TopLines( + Contains("commit 04").IsSelected(), + Contains("commit 03"), + Contains("commit 02"), + ). + // Verify commit messages are dropped + Tap(func() { + t.Views().Main(). + Content( + Contains("commit 04"). + DoesNotContain("commit 06"). + DoesNotContain("commit 05"), + ) + }). + Press(keys.Universal.RangeSelectDown). + TopLines( + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + Contains("commit 02"), + ). + // Move commits + Press(keys.Commits.MoveDownCommit). + TopLines( + Contains("commit 02"), + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + Contains("commit 01"), + ). + Press(keys.Commits.MoveDownCommit). + TopLines( + Contains("commit 02"), + Contains("commit 01"), + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + ). + Press(keys.Commits.MoveDownCommit). + TopLines( + Contains("commit 02"), + Contains("commit 01"), + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + ). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). + Press(keys.Commits.MoveUpCommit). + TopLines( + Contains("commit 02"), + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + Contains("commit 01"), + ). + Press(keys.Commits.MoveUpCommit). + TopLines( + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + Contains("commit 02"), + Contains("commit 01"), + ). + Press(keys.Commits.MoveUpCommit). + Tap(func() { + t.ExpectToast(Contains("Disabled: Cannot move any further")) + }). + TopLines( + Contains("commit 04").IsSelected(), + Contains("commit 03").IsSelected(), + Contains("commit 02"), + Contains("commit 01"), + ) + }, +}) diff --git a/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go b/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go index 931c52015..6ba313f7a 100644 --- a/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go +++ b/pkg/integration/tests/interactive_rebase/squash_down_second_commit.go @@ -27,7 +27,7 @@ var SquashDownSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{ Tap(func() { t.ExpectPopup().Confirmation(). Title(Equals("Squash")). - Content(Equals("Are you sure you want to squash this commit into the commit below?")). + Content(Equals("Are you sure you want to squash the selected commit(s) into the commit below?")). Confirm() }). Lines( diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 7f5cc23fa..f0437b30d 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -165,6 +165,7 @@ var tests = []*components.IntegrationTest{ interactive_rebase.Move, interactive_rebase.MoveInRebase, interactive_rebase.MoveWithCustomCommentChar, + interactive_rebase.OutsideRebaseRangeSelect, interactive_rebase.PickRescheduled, interactive_rebase.QuickStart, interactive_rebase.Rebase, diff --git a/pkg/integration/tests/undo/undo_checkout_and_drop.go b/pkg/integration/tests/undo/undo_checkout_and_drop.go index 4dc5a0a33..6c095b029 100644 --- a/pkg/integration/tests/undo/undo_checkout_and_drop.go +++ b/pkg/integration/tests/undo/undo_checkout_and_drop.go @@ -24,8 +24,8 @@ var UndoCheckoutAndDrop = NewIntegrationTest(NewIntegrationTestArgs{ confirmCommitDrop := func() { t.ExpectPopup().Confirmation(). - Title(Equals("Delete commit")). - Content(Equals("Are you sure you want to delete this commit?")). + Title(Equals("Drop commit")). + Content(Equals("Are you sure you want to drop the selected commit(s)?")). Confirm() } diff --git a/pkg/integration/tests/undo/undo_drop.go b/pkg/integration/tests/undo/undo_drop.go index 3a99fce8b..2d4d2d6a9 100644 --- a/pkg/integration/tests/undo/undo_drop.go +++ b/pkg/integration/tests/undo/undo_drop.go @@ -19,8 +19,8 @@ var UndoDrop = NewIntegrationTest(NewIntegrationTestArgs{ Run: func(t *TestDriver, keys config.KeybindingConfig) { confirmCommitDrop := func() { t.ExpectPopup().Confirmation(). - Title(Equals("Delete commit")). - Content(Equals("Are you sure you want to delete this commit?")). + Title(Equals("Drop commit")). + Content(Equals("Are you sure you want to drop the selected commit(s)?")). Confirm() } diff --git a/pkg/utils/rebase_todo.go b/pkg/utils/rebase_todo.go index 08a9ca872..e4bfe25d0 100644 --- a/pkg/utils/rebase_todo.go +++ b/pkg/utils/rebase_todo.go @@ -9,27 +9,46 @@ import ( "github.com/samber/lo" ) -// Read a git-rebase-todo file, change the action for the given sha to -// newAction, and write it back -func EditRebaseTodo(filePath string, sha string, oldAction todo.TodoCommand, newAction todo.TodoCommand, commentChar byte) error { +type Todo struct { + Sha string + Action todo.TodoCommand +} + +// In order to change a TODO in git-rebase-todo, we need to specify the old action, +// because sometimes the same sha appears multiple times in the file (e.g. in a pick +// and later in a merge) +type TodoChange struct { + Sha string + OldAction todo.TodoCommand + NewAction todo.TodoCommand +} + +// Read a git-rebase-todo file, change the actions for the given commits, +// and write it back +func EditRebaseTodo(filePath string, changes []TodoChange, commentChar byte) error { todos, err := ReadRebaseTodoFile(filePath, commentChar) if err != nil { return err } + matchCount := 0 for i := range todos { t := &todos[i] - // Comparing just the sha is not enough; we need to compare both the - // action and the sha, as the sha could appear multiple times (e.g. in a - // pick and later in a merge) - if t.Command == oldAction && equalShas(t.Commit, sha) { - t.Command = newAction - return WriteRebaseTodoFile(filePath, todos, commentChar) + // This is a nested loop, but it's ok because the number of todos should be small + for _, change := range changes { + if t.Command == change.OldAction && equalShas(t.Commit, change.Sha) { + matchCount++ + t.Command = change.NewAction + } } } - // Should never get here - return fmt.Errorf("Todo %s not found in git-rebase-todo", sha) + if matchCount < len(changes) { + // Should never get here + return fmt.Errorf("Some todos not found in git-rebase-todo") + } + + return WriteRebaseTodoFile(filePath, todos, commentChar) } func equalShas(a, b string) bool { @@ -73,24 +92,24 @@ func PrependStrToTodoFile(filePath string, linesToPrepend []byte) error { return os.WriteFile(filePath, linesToPrepend, 0o644) } -func MoveTodoDown(fileName string, sha string, action todo.TodoCommand, commentChar byte) error { +func MoveTodosDown(fileName string, todosToMove []Todo, commentChar byte) error { todos, err := ReadRebaseTodoFile(fileName, commentChar) if err != nil { return err } - rearrangedTodos, err := moveTodoDown(todos, sha, action) + rearrangedTodos, err := moveTodosDown(todos, todosToMove) if err != nil { return err } return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar) } -func MoveTodoUp(fileName string, sha string, action todo.TodoCommand, commentChar byte) error { +func MoveTodosUp(fileName string, todosToMove []Todo, commentChar byte) error { todos, err := ReadRebaseTodoFile(fileName, commentChar) if err != nil { return err } - rearrangedTodos, err := moveTodoUp(todos, sha, action) + rearrangedTodos, err := moveTodosUp(todos, todosToMove) if err != nil { return err } @@ -102,6 +121,11 @@ func moveTodoDown(todos []todo.Todo, sha string, action todo.TodoCommand) ([]tod return lo.Reverse(rearrangedTodos), err } +func moveTodosDown(todos []todo.Todo, todosToMove []Todo) ([]todo.Todo, error) { + rearrangedTodos, err := moveTodosUp(lo.Reverse(todos), lo.Reverse(todosToMove)) + return lo.Reverse(rearrangedTodos), err +} + func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.Todo, error) { _, sourceIdx, ok := lo.FindIndexOf(todos, func(t todo.Todo) bool { // Comparing just the sha is not enough; we need to compare both the @@ -134,6 +158,19 @@ func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo. return rearrangedTodos, nil } +func moveTodosUp(todos []todo.Todo, todosToMove []Todo) ([]todo.Todo, error) { + for _, todoToMove := range todosToMove { + var newTodos []todo.Todo + newTodos, err := moveTodoUp(todos, todoToMove.Sha, todoToMove.Action) + if err != nil { + return nil, err + } + todos = newTodos + } + + return todos, nil +} + func MoveFixupCommitDown(fileName string, originalSha string, fixupSha string, commentChar byte) error { todos, err := ReadRebaseTodoFile(fileName, commentChar) if err != nil {