1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-28 09:08:41 +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 DaemonKindExitImmediately
DaemonKindCherryPick DaemonKindCherryPick
DaemonKindMoveTodoUp DaemonKindMoveTodosUp
DaemonKindMoveTodoDown DaemonKindMoveTodosDown
DaemonKindInsertBreak DaemonKindInsertBreak
DaemonKindChangeTodoActions DaemonKindChangeTodoActions
DaemonKindMoveFixupCommitDown DaemonKindMoveFixupCommitDown
@ -56,8 +56,8 @@ func getInstruction() Instruction {
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction], DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction], DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction], DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction], DaemonKindMoveTodosUp: deserializeInstruction[*MoveTodosUpInstruction],
DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction], DaemonKindMoveTodosDown: deserializeInstruction[*MoveTodosDownInstruction],
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction], DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
} }
@ -208,13 +208,15 @@ func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error { func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error { return handleInteractiveRebase(common, func(path string) error {
for _, c := range self.Changes { changes := lo.Map(self.Changes, func(c ChangeTodoAction, _ int) utils.TodoChange {
if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction, getCommentChar()); err != nil { return utils.TodoChange{
return err 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 { type MoveTodosUpInstruction struct {
Sha string Shas []string
} }
func NewMoveTodoUpInstruction(sha string) Instruction { func NewMoveTodosUpInstruction(shas []string) Instruction {
return &MoveTodoUpInstruction{ return &MoveTodosUpInstruction{
Sha: sha, Shas: shas,
} }
} }
func (self *MoveTodoUpInstruction) Kind() DaemonKind { func (self *MoveTodosUpInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoUp return DaemonKindMoveTodosUp
} }
func (self *MoveTodoUpInstruction) SerializedInstructions() string { func (self *MoveTodosUpInstruction) SerializedInstructions() string {
return serializeInstruction(self) 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 handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodoUp(path, self.Sha, todo.Pick, getCommentChar()) return utils.MoveTodosUp(path, todosToMove, getCommentChar())
}) })
} }
type MoveTodoDownInstruction struct { type MoveTodosDownInstruction struct {
Sha string Shas []string
} }
func NewMoveTodoDownInstruction(sha string) Instruction { func NewMoveTodosDownInstruction(shas []string) Instruction {
return &MoveTodoDownInstruction{ return &MoveTodosDownInstruction{
Sha: sha, Shas: shas,
} }
} }
func (self *MoveTodoDownInstruction) Kind() DaemonKind { func (self *MoveTodosDownInstruction) Kind() DaemonKind {
return DaemonKindMoveTodoDown return DaemonKindMoveTodosDown
} }
func (self *MoveTodoDownInstruction) SerializedInstructions() string { func (self *MoveTodosDownInstruction) SerializedInstructions() string {
return serializeInstruction(self) 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 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() return self.ContinueRebase()
} }
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error { func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx int, endIdx int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+2) baseShaOrRoot := getBaseShaOrRoot(commits, endIdx+2)
sha := commits[index].Sha shas := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
return commit.Sha
msg := utils.ResolvePlaceholderString( })
self.Tr.Log.MoveCommitDown,
map[string]string{
"shortSha": utils.ShortSha(sha),
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot, baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoDownInstruction(sha), instruction: daemon.NewMoveTodosDownInstruction(shas),
overrideEditor: true, overrideEditor: true,
}).Run() }).Run()
} }
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error { func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int, endIdx int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+1) baseShaOrRoot := getBaseShaOrRoot(commits, endIdx+1)
sha := commits[index].Sha shas := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string {
return commit.Sha
msg := utils.ResolvePlaceholderString( })
self.Tr.Log.MoveCommitUp,
map[string]string{
"shortSha": utils.ShortSha(sha),
},
)
self.os.LogCommand(msg, false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot, baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoUpInstruction(sha), instruction: daemon.NewMoveTodosUpInstruction(shas),
overrideEditor: true, overrideEditor: true,
}).Run() }).Run()
} }
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error { func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx int, endIdx int, action todo.TodoCommand) error {
baseIndex := index + 1 baseIndex := endIdx + 1
if action == todo.Squash || action == todo.Fixup { if action == todo.Squash || action == todo.Fixup {
baseIndex++ baseIndex++
} }
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex) baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
changes := []daemon.ChangeTodoAction{{ changes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) daemon.ChangeTodoAction {
Sha: commits[index].Sha, return daemon.ChangeTodoAction{
NewAction: action, Sha: commit.Sha,
}} NewAction: action,
}
})
self.os.LogCommand(logTodoChanges(changes), false) self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ 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 { changeTodoStr := strings.Join(lo.Map(changes, func(c daemon.ChangeTodoAction, _ int) string {
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction) return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
}), "\n") }), "\n")
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr) return fmt.Sprintf("Changing TODO actions:\n%s", changeTodoStr)
} }
type PrepareInteractiveRebaseCommandOpts struct { type PrepareInteractiveRebaseCommandOpts struct {
@ -281,22 +272,45 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
}).Run() }).Run()
} }
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file // Sets the action for the given commits in the git-rebase-todo file
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error { 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( 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) MoveTodosDown(commits []*models.Commit) error {
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") 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) MoveTodosUp(commits []*models.Commit) error {
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") 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 // SquashAllAboveFixupCommits squashes all fixup! commits above the given one

View File

@ -2,6 +2,8 @@ package helpers
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
@ -80,6 +82,19 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
} }
self.c.LogAction(fmt.Sprintf("Merge/Rebase: %s", command)) 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 := "" commandType := ""
switch status { switch status {

View File

@ -45,6 +45,7 @@ func NewLocalCommitsController(
c, c,
c.Contexts().LocalCommits, c.Contexts().LocalCommits,
c.Contexts().LocalCommits.GetSelected, c.Contexts().LocalCommits.GetSelected,
c.Contexts().LocalCommits.GetSelectedItems,
), ),
} }
} }
@ -55,17 +56,23 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
outsideFilterModeBindings := []*types.Binding{ outsideFilterModeBindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Commits.SquashDown), Key: opts.GetKey(opts.Config.Commits.SquashDown),
Handler: self.withItem(self.squashDown), Handler: self.withItemsRange(self.squashDown),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.getDisabledReasonForSquashDown), self.itemRangeSelected(
self.midRebaseCommandEnabled,
self.canSquashOrFixup,
),
), ),
Description: self.c.Tr.SquashDown, Description: self.c.Tr.SquashDown,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup), Key: opts.GetKey(opts.Config.Commits.MarkCommitAsFixup),
Handler: self.withItem(self.fixup), Handler: self.withItemsRange(self.fixup),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.getDisabledReasonForFixup), self.itemRangeSelected(
self.midRebaseCommandEnabled,
self.canSquashOrFixup,
),
), ),
Description: self.c.Tr.FixupCommit, Description: self.c.Tr.FixupCommit,
}, },
@ -73,7 +80,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Key: opts.GetKey(opts.Config.Commits.RenameCommit), Key: opts.GetKey(opts.Config.Commits.RenameCommit),
Handler: self.withItem(self.reword), Handler: self.withItem(self.reword),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), self.singleItemSelected(self.rewordEnabled),
), ),
Description: self.c.Tr.RewordCommit, Description: self.c.Tr.RewordCommit,
}, },
@ -81,23 +88,26 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor), Key: opts.GetKey(opts.Config.Commits.RenameCommitWithEditor),
Handler: self.withItem(self.rewordEditor), Handler: self.withItem(self.rewordEditor),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.rebaseCommandEnabled(todo.Reword)), self.singleItemSelected(self.rewordEnabled),
), ),
Description: self.c.Tr.RenameCommitEditor, Description: self.c.Tr.RenameCommitEditor,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Remove), Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.withItem(self.drop), Handler: self.withItemsRange(self.drop),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.rebaseCommandEnabled(todo.Drop)), self.itemRangeSelected(
self.midRebaseCommandEnabled,
),
), ),
Description: self.c.Tr.DeleteCommit, Description: self.c.Tr.DeleteCommit,
}, },
{ {
Key: opts.GetKey(editCommitKey), 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( GetDisabledReason: self.require(
self.singleItemSelected(self.rebaseCommandEnabled(todo.Edit)), self.itemRangeSelected(self.midRebaseCommandEnabled),
), ),
Description: self.c.Tr.EditCommit, Description: self.c.Tr.EditCommit,
}, },
@ -107,7 +117,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
// when you manually select the base commit. // when you manually select the base commit.
Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase), Key: opts.GetKey(opts.Config.Commits.StartInteractiveRebase),
Handler: self.withItem(self.quickStartInteractiveRebase), 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, Description: self.c.Tr.QuickStartInteractiveRebase,
Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{ Tooltip: utils.ResolvePlaceholderString(self.c.Tr.QuickStartInteractiveRebaseTooltip, map[string]string{
"editKey": keybindings.Label(editCommitKey), "editKey": keybindings.Label(editCommitKey),
@ -115,9 +125,9 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.PickCommit), Key: opts.GetKey(opts.Config.Commits.PickCommit),
Handler: self.withItem(self.pick), Handler: self.withItems(self.pick),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.singleItemSelected(self.rebaseCommandEnabled(todo.Pick)), self.itemRangeSelected(self.pickEnabled),
), ),
Description: self.c.Tr.PickCommit, Description: self.c.Tr.PickCommit,
}, },
@ -131,22 +141,28 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits), Key: opts.GetKey(opts.Config.Commits.SquashAboveCommits),
Handler: self.withItem(self.squashAllAboveFixupCommits), Handler: self.withItem(self.squashAllAboveFixupCommits),
GetDisabledReason: self.require( GetDisabledReason: self.require(
self.notMidRebase, self.notMidRebase(self.c.Tr.AlreadyRebasing),
self.singleItemSelected(), self.singleItemSelected(),
), ),
Description: self.c.Tr.SquashAboveCommits, Description: self.c.Tr.SquashAboveCommits,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveDownCommit), Key: opts.GetKey(opts.Config.Commits.MoveDownCommit),
Handler: self.withItem(self.moveDown), Handler: self.withItemsRange(self.moveDown),
GetDisabledReason: self.require(self.singleItemSelected()), GetDisabledReason: self.require(self.itemRangeSelected(
Description: self.c.Tr.MoveDownCommit, self.midRebaseCommandEnabled,
self.canMoveDown,
)),
Description: self.c.Tr.MoveDownCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.MoveUpCommit), Key: opts.GetKey(opts.Config.Commits.MoveUpCommit),
Handler: self.withItem(self.moveUp), Handler: self.withItemsRange(self.moveUp),
GetDisabledReason: self.require(self.singleItemSelected()), GetDisabledReason: self.require(self.itemRangeSelected(
Description: self.c.Tr.MoveUpCommit, self.midRebaseCommandEnabled,
self.canMoveUp,
)),
Description: self.c.Tr.MoveUpCommit,
}, },
{ {
Key: opts.GetKey(opts.Config.Commits.PasteCommits), Key: opts.GetKey(opts.Config.Commits.PasteCommits),
@ -263,13 +279,9 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
return nil return nil
} }
func (self *LocalCommitsController) squashDown(commit *models.Commit) error { func (self *LocalCommitsController) squashDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
applied, err := self.handleMidRebaseCommand(todo.Squash, commit) if self.isRebasing() {
if err != nil { return self.updateTodos(todo.Squash, selectedCommits)
return err
}
if applied {
return nil
} }
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
@ -278,27 +290,15 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown) 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 { func (self *LocalCommitsController) fixup(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { if self.isRebasing() {
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit} return self.updateTodos(todo.Fixup, selectedCommits)
}
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
} }
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
@ -307,29 +307,13 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.FixupCommit) 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 { 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) commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Sha)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
@ -404,14 +388,6 @@ func (self *LocalCommitsController) doRewordEditor() error {
} }
func (self *LocalCommitsController) rewordEditor(commit *models.Commit) 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 { if self.c.UserConfig.Gui.SkipRewordInEditorWarning {
return self.doRewordEditor() return self.doRewordEditor()
} else { } else {
@ -423,37 +399,37 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
} }
} }
func (self *LocalCommitsController) drop(commit *models.Commit) error { func (self *LocalCommitsController) drop(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
applied, err := self.handleMidRebaseCommand(todo.Drop, commit) if self.isRebasing() {
if err != nil { return self.updateTodos(todo.Drop, selectedCommits)
return err
}
if applied {
return nil
} }
return self.c.Confirm(types.ConfirmOpts{ return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DeleteCommitTitle, Title: self.c.Tr.DropCommitTitle,
Prompt: self.c.Tr.DeleteCommitPrompt, Prompt: self.c.Tr.DropCommitPrompt,
HandleConfirm: func() error { 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) 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 { func (self *LocalCommitsController) edit(selectedCommits []*models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Edit, commit) if self.isRebasing() {
if err != nil { return self.updateTodos(todo.Edit, selectedCommits)
return err
}
if applied {
return nil
} }
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 { func (self *LocalCommitsController) quickStartInteractiveRebase(selectedCommit *models.Commit) error {
@ -504,13 +480,9 @@ func (self *LocalCommitsController) findCommitForQuickStartInteractiveRebase() (
return commit, nil return commit, nil
} }
func (self *LocalCommitsController) pick(commit *models.Commit) error { func (self *LocalCommitsController) pick(selectedCommits []*models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Pick, commit) if self.isRebasing() {
if err != nil { return self.updateTodos(todo.Pick, selectedCommits)
return err
}
if applied {
return nil
} }
// at this point we aren't actually rebasing so we will interpret this as an // 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() return self.pullFiles()
} }
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error { func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, startIdx int, endIdx int) error {
err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action) // 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) 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 // 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 // begin a rebase. It then updates the todo file with that action
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { func (self *LocalCommitsController) updateTodos(action todo.TodoCommand, selectedCommits []*models.Commit) error {
if !commit.IsTODO() { if err := self.c.Git().Rebase.EditRebaseTodo(selectedCommits, action); err != nil {
return false, nil return self.c.Error(err)
} }
self.c.LogAction("Update rebase TODO") return self.c.Refresh(types.RefreshOptions{
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{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
}) })
} }
func (self *LocalCommitsController) rebaseCommandEnabled(action todo.TodoCommand) func(*models.Commit) *types.DisabledReason { func (self *LocalCommitsController) rewordEnabled(commit *models.Commit) *types.DisabledReason {
return func(commit *models.Commit) *types.DisabledReason { // for now we do not support setting 'reword' on TODO commits because it requires an editor
if commit.Action == models.ActionConflict { // and that means we either unconditionally wait around for the subprocess to ask for
return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed} // 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() {
if !commit.IsTODO() { return &types.DisabledReason{Text: self.c.Tr.RewordNotSupported}
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
} }
// 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 { func (self *LocalCommitsController) isRebasing() bool {
index := self.context().GetSelectedLineIdx() return self.c.Model().WorkingTreeStateAtLastCommitRefresh != enums.REBASE_MODE_NONE
commits := self.c.Model().Commits }
// can't move past the initial commit func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
if index >= len(commits)-1 { if self.isRebasing() {
return nil if err := self.c.Git().Rebase.MoveTodosDown(selectedCommits); err != 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 {
return self.c.Error(err) return self.c.Error(err)
} }
self.context().MoveSelectedLine(1) self.context().MoveSelection(1)
return self.c.Refresh(types.RefreshOptions{ return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, 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 { return self.c.WithWaitingStatusSync(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) 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 { if err == nil {
self.context().MoveSelectedLine(1) self.context().MoveSelection(1)
} }
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err, types.RefreshOptions{Mode: types.SYNC}) err, types.RefreshOptions{Mode: types.SYNC})
}) })
} }
func (self *LocalCommitsController) moveUp(commit *models.Commit) error { func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
index := self.context().GetSelectedLineIdx() if self.isRebasing() {
if index == 0 { if err := self.c.Git().Rebase.MoveTodosUp(selectedCommits); err != nil {
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 {
return self.c.Error(err) return self.c.Error(err)
} }
self.context().MoveSelectedLine(-1) self.context().MoveSelection(-1)
return self.c.Refresh(types.RefreshOptions{ return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS}, 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 { return self.c.WithWaitingStatusSync(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) 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 { if err == nil {
self.context().MoveSelectedLine(-1) self.context().MoveSelection(-1)
} }
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err, types.RefreshOptions{Mode: types.SYNC}) 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{ return self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.AmendCommitTitle, Title: self.c.Tr.AmendCommitTitle,
Prompt: self.c.Tr.AmendCommitPrompt, 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 { 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} 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 { 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{ return self.c.Menu(types.CreateMenuOptions{
Title: "Amend commit attribute", Title: "Amend commit attribute",
Items: []*types.MenuItem{ Items: []*types.MenuItem{
@ -846,7 +744,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C
} }
func (self *LocalCommitsController) afterRevertCommit() error { func (self *LocalCommitsController) afterRevertCommit() error {
self.context().MoveSelectedLine(1) self.context().MoveSelection(1)
return self.c.Refresh(types.RefreshOptions{ return self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES}, 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 { func (self *LocalCommitsController) createTag(commit *models.Commit) error {
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {}) 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()) return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx())
} }
func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool { func (self *LocalCommitsController) notMidRebase(message string) func() *types.DisabledReason {
allowedActions := []todo.TodoCommand{ return func() *types.DisabledReason {
todo.Pick, if self.isRebasing() {
todo.Drop, return &types.DisabledReason{Text: message}
todo.Edit, }
todo.Fixup,
todo.Squash, return nil
todo.Reword, }
}
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: "向下滚动主面板", ScrollDownMainPanel: "向下滚动主面板",
AmendCommitTitle: "修改提交", AmendCommitTitle: "修改提交",
AmendCommitPrompt: "您确定要使用暂存文件来修改此提交吗?", AmendCommitPrompt: "您确定要使用暂存文件来修改此提交吗?",
DeleteCommitTitle: "删除提交", DropCommitTitle: "删除提交",
DeleteCommitPrompt: "您确定要删除此提交吗?", DropCommitPrompt: "您确定要删除此提交吗?",
PullingStatus: "正在拉取", PullingStatus: "正在拉取",
PushingStatus: "正在推送", PushingStatus: "正在推送",
FetchingStatus: "正在抓取", FetchingStatus: "正在抓取",

View File

@ -181,8 +181,8 @@ func dutchTranslationSet() TranslationSet {
ScrollDownMainPanel: "Scroll naar beneden vanaf hoofdpaneel", ScrollDownMainPanel: "Scroll naar beneden vanaf hoofdpaneel",
AmendCommitTitle: "Commit wijzigen", AmendCommitTitle: "Commit wijzigen",
AmendCommitPrompt: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?", AmendCommitPrompt: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?",
DeleteCommitTitle: "Verwijder commit", DropCommitTitle: "Verwijder commit",
DeleteCommitPrompt: "Weet je zeker dat je deze commit wil verwijderen?", DropCommitPrompt: "Weet je zeker dat je deze commit wil verwijderen?",
PullingStatus: "Pullen", PullingStatus: "Pullen",
PushingStatus: "Pushen", PushingStatus: "Pushen",
FetchingStatus: "Fetchen", FetchingStatus: "Fetchen",

View File

@ -119,6 +119,7 @@ type TranslationSet struct {
DeleteCommit string DeleteCommit string
MoveDownCommit string MoveDownCommit string
MoveUpCommit string MoveUpCommit string
CannotMoveAnyFurther string
EditCommit string EditCommit string
AmendToCommit string AmendToCommit string
ResetAuthor string ResetAuthor string
@ -239,6 +240,7 @@ type TranslationSet struct {
SimpleRebase string SimpleRebase string
InteractiveRebase string InteractiveRebase string
InteractiveRebaseTooltip string InteractiveRebaseTooltip string
MustSelectTodoCommits string
ConfirmMerge string ConfirmMerge string
FwdNoUpstream string FwdNoUpstream string
FwdNoLocalUpstream string FwdNoLocalUpstream string
@ -270,14 +272,15 @@ type TranslationSet struct {
ScrollDownMainPanel string ScrollDownMainPanel string
AmendCommitTitle string AmendCommitTitle string
AmendCommitPrompt string AmendCommitPrompt string
DeleteCommitTitle string DropCommitTitle string
DeleteCommitPrompt string DropCommitPrompt string
PullingStatus string PullingStatus string
PushingStatus string PushingStatus string
FetchingStatus string FetchingStatus string
SquashingStatus string SquashingStatus string
FixingStatus string FixingStatus string
DeletingStatus string DeletingStatus string
DroppingStatus string
MovingStatus string MovingStatus string
RebasingStatus string RebasingStatus string
MergingStatus string MergingStatus string
@ -686,8 +689,6 @@ type Log struct {
CherryPickCommits string CherryPickCommits string
HandleUndo string HandleUndo string
HandleMidRebaseCommand string HandleMidRebaseCommand string
MovingCommitUp string
MovingCommitDown string
RemoveFile string RemoveFile string
CopyToClipboard string CopyToClipboard string
Remove string Remove string
@ -945,8 +946,8 @@ func EnglishTranslationSet() TranslationSet {
UpdateRefHere: "Update branch '{{.ref}}' here", UpdateRefHere: "Update branch '{{.ref}}' here",
CannotSquashOrFixupFirstCommit: "There's no commit below to squash into", CannotSquashOrFixupFirstCommit: "There's no commit below to squash into",
Fixup: "Fixup", Fixup: "Fixup",
SureFixupThisCommit: "Are you sure you want to 'fixup' this commit? It will be merged 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 this commit into the commit below?", SureSquashThisCommit: "Are you sure you want to squash the selected commit(s) into the commit below?",
Squash: "Squash", Squash: "Squash",
PickCommit: "Pick commit (when mid-rebase)", PickCommit: "Pick commit (when mid-rebase)",
RevertCommit: "Revert commit", RevertCommit: "Revert commit",
@ -954,6 +955,7 @@ func EnglishTranslationSet() TranslationSet {
DeleteCommit: "Delete commit", DeleteCommit: "Delete commit",
MoveDownCommit: "Move commit down one", MoveDownCommit: "Move commit down one",
MoveUpCommit: "Move commit up one", MoveUpCommit: "Move commit up one",
CannotMoveAnyFurther: "Cannot move any further",
EditCommit: "Edit commit", EditCommit: "Edit commit",
AmendToCommit: "Amend commit with staged changes", AmendToCommit: "Amend commit with staged changes",
ResetAuthor: "Reset author", ResetAuthor: "Reset author",
@ -1079,6 +1081,7 @@ func EnglishTranslationSet() TranslationSet {
SimpleRebase: "Simple rebase", SimpleRebase: "Simple rebase",
InteractiveRebase: "Interactive rebase", InteractiveRebase: "Interactive rebase",
InteractiveRebaseTooltip: "Begin an interactive rebase with a break at the start, so you can update the TODO commits before continuing", 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}}'?", ConfirmMerge: "Are you sure you want to merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'?",
FwdNoUpstream: "Cannot fast-forward a branch with no upstream", FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally", FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
@ -1110,14 +1113,15 @@ func EnglishTranslationSet() TranslationSet {
ScrollDownMainPanel: "Scroll down main panel", ScrollDownMainPanel: "Scroll down main panel",
AmendCommitTitle: "Amend commit", AmendCommitTitle: "Amend commit",
AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?", AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?",
DeleteCommitTitle: "Delete commit", DropCommitTitle: "Drop commit",
DeleteCommitPrompt: "Are you sure you want to delete this commit?", DropCommitPrompt: "Are you sure you want to drop the selected commit(s)?",
PullingStatus: "Pulling", PullingStatus: "Pulling",
PushingStatus: "Pushing", PushingStatus: "Pushing",
FetchingStatus: "Fetching", FetchingStatus: "Fetching",
SquashingStatus: "Squashing", SquashingStatus: "Squashing",
FixingStatus: "Fixing up", FixingStatus: "Fixing up",
DeletingStatus: "Deleting", DeletingStatus: "Deleting",
DroppingStatus: "Dropping",
MovingStatus: "Moving", MovingStatus: "Moving",
RebasingStatus: "Rebasing", RebasingStatus: "Rebasing",
MergingStatus: "Merging", MergingStatus: "Merging",
@ -1626,8 +1630,6 @@ func EnglishTranslationSet() TranslationSet {
CherryPickCommits: "Cherry-picking commits:\n'{{.commitLines}}'", CherryPickCommits: "Cherry-picking commits:\n'{{.commitLines}}'",
HandleUndo: "Undoing last conflict resolution", HandleUndo: "Undoing last conflict resolution",
HandleMidRebaseCommand: "Updating rebase action of commit {{.shortSha}} to '{{.action}}'", HandleMidRebaseCommand: "Updating rebase action of commit {{.shortSha}} to '{{.action}}'",
MovingCommitUp: "Moving commit {{.shortSha}} up",
MovingCommitDown: "Moving commit {{.shortSha}} down",
RemoveFile: "Deleting path '{{.path}}'", RemoveFile: "Deleting path '{{.path}}'",
CopyToClipboard: "Copying '{{.str}}' to clipboard", CopyToClipboard: "Copying '{{.str}}' to clipboard",
Remove: "Removing '{{.filename}}'", Remove: "Removing '{{.filename}}'",

View File

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

View File

@ -218,8 +218,8 @@ func koreanTranslationSet() TranslationSet {
ScrollDownMainPanel: "메인 패널을 아래로로 스크롤", ScrollDownMainPanel: "메인 패널을 아래로로 스크롤",
AmendCommitTitle: "Amend commit", AmendCommitTitle: "Amend commit",
AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?", AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?",
DeleteCommitTitle: "커밋 삭제", DropCommitTitle: "커밋 삭제",
DeleteCommitPrompt: "정말로 선택한 커밋을 삭제하시겠습니까?", DropCommitPrompt: "정말로 선택한 커밋을 삭제하시겠습니까?",
PullingStatus: "업데이트 중", PullingStatus: "업데이트 중",
PushingStatus: "푸시 중", PushingStatus: "푸시 중",
FetchingStatus: "패치 중", FetchingStatus: "패치 중",

View File

@ -147,8 +147,8 @@ func polishTranslationSet() TranslationSet {
ScrollUp: "Przewiń w górę", ScrollUp: "Przewiń w górę",
AmendCommitTitle: "Popraw commit", AmendCommitTitle: "Popraw commit",
AmendCommitPrompt: "Czy na pewno chcesz poprawić ten commit plikami z poczekalni?", AmendCommitPrompt: "Czy na pewno chcesz poprawić ten commit plikami z poczekalni?",
DeleteCommitTitle: "Usuń commit", DropCommitTitle: "Usuń commit",
DeleteCommitPrompt: "Czy na pewno usunąć ten commit?", DropCommitPrompt: "Czy na pewno usunąć ten commit?",
PullingStatus: "Pobieranie zmian", PullingStatus: "Pobieranie zmian",
PushingStatus: "Wysyłanie zmian", PushingStatus: "Wysyłanie zmian",
FetchingStatus: "Pobieram", FetchingStatus: "Pobieram",

View File

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

View File

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

View File

@ -22,8 +22,8 @@ var Undo = NewIntegrationTest(NewIntegrationTestArgs{
confirmCommitDrop := func() { confirmCommitDrop := func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Delete commit")). Title(Equals("Drop commit")).
Content(Equals("Are you sure you want to delete this commit?")). Content(Equals("Are you sure you want to drop the selected commit(s)?")).
Wait(500). Wait(500).
Confirm() Confirm()
} }

View File

@ -23,8 +23,8 @@ var DropWithCustomCommentChar = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Universal.Remove). Press(keys.Universal.Remove).
Tap(func() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Delete commit")). Title(Equals("Drop commit")).
Content(Equals("Are you sure you want to delete this commit?")). Content(Equals("Are you sure you want to drop the selected commit(s)?")).
Confirm() Confirm()
}). }).
Lines( Lines(

View File

@ -29,6 +29,6 @@ var EditNonTodoCommitDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
NavigateToLine(Contains("commit 01")). NavigateToLine(Contains("commit 01")).
Press(keys.Universal.Edit) 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")). NavigateToLine(Contains("<-- YOU ARE HERE --- commit three")).
Press(keys.Commits.RenameCommit) 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() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Fixup")). 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() Confirm()
}). }).
Lines( 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 // assert nothing happens upon trying to move beyond the last commit
Press(keys.Commits.MoveDownCommit). Press(keys.Commits.MoveDownCommit).
Tap(func() {
t.ExpectToast(Contains("Disabled: Cannot move any further"))
}).
Lines( Lines(
Contains("commit 03"), Contains("commit 03"),
Contains("commit 02"), Contains("commit 02"),
@ -74,6 +77,9 @@ var Move = NewIntegrationTest(NewIntegrationTestArgs{
). ).
// assert nothing happens upon trying to move beyond the first commit // assert nothing happens upon trying to move beyond the first commit
Press(keys.Commits.MoveUpCommit). Press(keys.Commits.MoveUpCommit).
Tap(func() {
t.ExpectToast(Contains("Disabled: Cannot move any further"))
}).
Lines( Lines(
Contains("commit 04").IsSelected(), Contains("commit 04").IsSelected(),
Contains("commit 03"), Contains("commit 03"),

View File

@ -45,8 +45,11 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 03"), Contains("commit 03"),
Contains("YOU ARE HERE").Contains("commit 01"), Contains("YOU ARE HERE").Contains("commit 01"),
). ).
Press(keys.Commits.MoveUpCommit).
// assert we can't move past the top // assert we can't move past the top
Press(keys.Commits.MoveUpCommit).
Tap(func() {
t.ExpectToast(Contains("Disabled: Cannot move any further"))
}).
Lines( Lines(
Contains("commit 02").IsSelected(), Contains("commit 02").IsSelected(),
Contains("commit 04"), Contains("commit 04"),
@ -69,6 +72,9 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{
). ).
// assert we can't move past the bottom // assert we can't move past the bottom
Press(keys.Commits.MoveDownCommit). Press(keys.Commits.MoveDownCommit).
Tap(func() {
t.ExpectToast(Contains("Disabled: Cannot move any further"))
}).
Lines( Lines(
Contains("commit 04"), Contains("commit 04"),
Contains("commit 03"), 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() { Tap(func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Squash")). 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() Confirm()
}). }).
Lines( Lines(

View File

@ -165,6 +165,7 @@ var tests = []*components.IntegrationTest{
interactive_rebase.Move, interactive_rebase.Move,
interactive_rebase.MoveInRebase, interactive_rebase.MoveInRebase,
interactive_rebase.MoveWithCustomCommentChar, interactive_rebase.MoveWithCustomCommentChar,
interactive_rebase.OutsideRebaseRangeSelect,
interactive_rebase.PickRescheduled, interactive_rebase.PickRescheduled,
interactive_rebase.QuickStart, interactive_rebase.QuickStart,
interactive_rebase.Rebase, interactive_rebase.Rebase,

View File

@ -24,8 +24,8 @@ var UndoCheckoutAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
confirmCommitDrop := func() { confirmCommitDrop := func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Delete commit")). Title(Equals("Drop commit")).
Content(Equals("Are you sure you want to delete this commit?")). Content(Equals("Are you sure you want to drop the selected commit(s)?")).
Confirm() Confirm()
} }

View File

@ -19,8 +19,8 @@ var UndoDrop = NewIntegrationTest(NewIntegrationTestArgs{
Run: func(t *TestDriver, keys config.KeybindingConfig) { Run: func(t *TestDriver, keys config.KeybindingConfig) {
confirmCommitDrop := func() { confirmCommitDrop := func() {
t.ExpectPopup().Confirmation(). t.ExpectPopup().Confirmation().
Title(Equals("Delete commit")). Title(Equals("Drop commit")).
Content(Equals("Are you sure you want to delete this commit?")). Content(Equals("Are you sure you want to drop the selected commit(s)?")).
Confirm() Confirm()
} }

View File

@ -9,27 +9,46 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
// Read a git-rebase-todo file, change the action for the given sha to type Todo struct {
// newAction, and write it back Sha string
func EditRebaseTodo(filePath string, sha string, oldAction todo.TodoCommand, newAction todo.TodoCommand, commentChar byte) error { 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) todos, err := ReadRebaseTodoFile(filePath, commentChar)
if err != nil { if err != nil {
return err return err
} }
matchCount := 0
for i := range todos { for i := range todos {
t := &todos[i] t := &todos[i]
// Comparing just the sha is not enough; we need to compare both the // This is a nested loop, but it's ok because the number of todos should be small
// action and the sha, as the sha could appear multiple times (e.g. in a for _, change := range changes {
// pick and later in a merge) if t.Command == change.OldAction && equalShas(t.Commit, change.Sha) {
if t.Command == oldAction && equalShas(t.Commit, sha) { matchCount++
t.Command = newAction t.Command = change.NewAction
return WriteRebaseTodoFile(filePath, todos, commentChar) }
} }
} }
// Should never get here if matchCount < len(changes) {
return fmt.Errorf("Todo %s not found in git-rebase-todo", sha) // 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 { func equalShas(a, b string) bool {
@ -73,24 +92,24 @@ func PrependStrToTodoFile(filePath string, linesToPrepend []byte) error {
return os.WriteFile(filePath, linesToPrepend, 0o644) 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) todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil { if err != nil {
return err return err
} }
rearrangedTodos, err := moveTodoDown(todos, sha, action) rearrangedTodos, err := moveTodosDown(todos, todosToMove)
if err != nil { if err != nil {
return err return err
} }
return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar) 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) todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil { if err != nil {
return err return err
} }
rearrangedTodos, err := moveTodoUp(todos, sha, action) rearrangedTodos, err := moveTodosUp(todos, todosToMove)
if err != nil { if err != nil {
return err return err
} }
@ -102,6 +121,11 @@ func moveTodoDown(todos []todo.Todo, sha string, action todo.TodoCommand) ([]tod
return lo.Reverse(rearrangedTodos), err 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) { func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.Todo, error) {
_, sourceIdx, ok := lo.FindIndexOf(todos, func(t todo.Todo) bool { _, sourceIdx, ok := lo.FindIndexOf(todos, func(t todo.Todo) bool {
// Comparing just the sha is not enough; we need to compare both the // 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 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 { func MoveFixupCommitDown(fileName string, originalSha string, fixupSha string, commentChar byte) error {
todos, err := ReadRebaseTodoFile(fileName, commentChar) todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil { if err != nil {