1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-24 08:52:21 +02:00

Support range select in rebase actions

This commit is contained in:
Jesse Duffield 2024-01-08 11:49:42 +11:00
parent 44e2542e4a
commit f0de880136
26 changed files with 776 additions and 366 deletions

View File

@ -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())
})
}

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -217,8 +217,8 @@ func chineseTranslationSet() TranslationSet {
ScrollDownMainPanel: "向下滚动主面板",
AmendCommitTitle: "修改提交",
AmendCommitPrompt: "您确定要使用暂存文件来修改此提交吗?",
DeleteCommitTitle: "删除提交",
DeleteCommitPrompt: "您确定要删除此提交吗?",
DropCommitTitle: "删除提交",
DropCommitPrompt: "您确定要删除此提交吗?",
PullingStatus: "正在拉取",
PushingStatus: "正在推送",
FetchingStatus: "正在抓取",

View File

@ -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",

View File

@ -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}}'",

View File

@ -221,8 +221,8 @@ func japaneseTranslationSet() TranslationSet {
ScrollDownMainPanel: "メインパネルを下にスクロール",
AmendCommitTitle: "Amendコミット",
AmendCommitPrompt: "ステージされたファイルで現在のコミットをamendします。よろしいですか?",
DeleteCommitTitle: "コミットを削除",
DeleteCommitPrompt: "選択されたコミットを削除します。よろしいですか?",
DropCommitTitle: "コミットを削除",
DropCommitPrompt: "選択されたコミットを削除します。よろしいですか?",
PullingStatus: "Pull中",
PushingStatus: "Push中",
FetchingStatus: "Fetch中",

View File

@ -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: "패치 중",

View File

@ -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",

View File

@ -262,8 +262,8 @@ func RussianTranslationSet() TranslationSet {
ScrollDownMainPanel: "Прокрутить вниз главную панель",
AmendCommitTitle: "Править коммит (amend)",
AmendCommitPrompt: "Вы уверены, что хотите править этот коммит проиндексированными файлами?",
DeleteCommitTitle: "Удалить коммит",
DeleteCommitPrompt: "Вы уверены, что хотите удалить этот коммит?",
DropCommitTitle: "Удалить коммит",
DropCommitPrompt: "Вы уверены, что хотите удалить этот коммит?",
PullingStatus: "Получение и слияние изменении",
PushingStatus: "Отправка изменении",
FetchingStatus: "Получение изменении",

View File

@ -293,8 +293,8 @@ func traditionalChineseTranslationSet() TranslationSet {
ScrollDownMainPanel: "向下捲動主面板",
AmendCommitTitle: "修正提交",
AmendCommitPrompt: "你確定要使用預存的檔案修正此提交嗎?",
DeleteCommitTitle: "刪除提交",
DeleteCommitPrompt: "你確定要刪除此提交嗎?",
DropCommitTitle: "刪除提交",
DropCommitPrompt: "你確定要刪除此提交嗎?",
PullingStatus: "拉取",
PushingStatus: "推送",
FetchingStatus: "擷取",

View File

@ -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()
}

View File

@ -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(

View File

@ -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."))
},
})

View File

@ -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"))
},
})

View File

@ -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(

View File

@ -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(),
)
},
})

View File

@ -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"),

View File

@ -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"),

View File

@ -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"),
)
},
})

View File

@ -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(

View File

@ -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,

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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 {