1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-10 04:07:18 +02:00

Support range select for rebase actions (#3232)

- **PR Description**

Adds support for range select in rebase actions both mid-rebase and
outside a rebase i.e.:
* pick
* drop
* fixup
* squash
* move up
* move down

Also includes a refactor to support a withItems wrapper for keybinding
handlers that support a range of items to be selected.

TODO:
* [x] Add integration tests
* [x] Add some LogAction calls (some are currently commented out)

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs (specifically `docs/Config.md`) have been updated if
necessary
* [x] You've read through your own file changes for silly mistakes etc

<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view'
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
This commit is contained in:
Jesse Duffield 2024-01-24 09:28:22 +11:00 committed by GitHub
commit 5511cc170e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1087 additions and 531 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

@ -59,15 +59,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
return self
}
func (self *BranchesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *BranchesContext) GetSelectedRef() types.Ref {
branch := self.GetSelected()
if branch == nil {

View File

@ -72,15 +72,6 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
return ctx
}
func (self *CommitFilesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *CommitFilesContext) GetDiffTerminals() []string {
return []string{self.GetRef().RefName()}
}

View File

@ -1,12 +1,12 @@
package context
type FilteredListViewModel[T any] struct {
type FilteredListViewModel[T HasID] struct {
*FilteredList[T]
*ListViewModel[T]
*SearchHistory
}
func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
func NewFilteredListViewModel[T HasID](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
filteredList := NewFilteredList(getList, getFilterFields)
self := &FilteredListViewModel[T]{

View File

@ -9,10 +9,17 @@ import (
"github.com/stretchr/testify/assert"
)
// wrapping string in my own type to give it an ID method which is required for list items
type mystring string
func (self mystring) ID() string {
return string(self)
}
func TestListRenderer_renderLines(t *testing.T) {
scenarios := []struct {
name string
modelStrings []string
modelStrings []mystring
nonModelIndices []int
startIdx int
endIdx int
@ -20,7 +27,7 @@ func TestListRenderer_renderLines(t *testing.T) {
}{
{
name: "Render whole list",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
startIdx: 0,
endIdx: 3,
expectedOutput: `
@ -30,7 +37,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Partial list, beginning",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
startIdx: 0,
endIdx: 2,
expectedOutput: `
@ -39,7 +46,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Partial list, end",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
startIdx: 1,
endIdx: 3,
expectedOutput: `
@ -48,7 +55,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Pass an endIdx greater than the model length",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
startIdx: 2,
endIdx: 5,
expectedOutput: `
@ -56,7 +63,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Whole list with section headers",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
nonModelIndices: []int{1, 3},
startIdx: 0,
endIdx: 5,
@ -69,7 +76,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Multiple consecutive headers",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
nonModelIndices: []int{0, 0, 2, 2, 2},
startIdx: 0,
endIdx: 8,
@ -85,7 +92,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Partial list with headers, beginning",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
nonModelIndices: []int{1, 3},
startIdx: 0,
endIdx: 3,
@ -96,7 +103,7 @@ func TestListRenderer_renderLines(t *testing.T) {
},
{
name: "Partial list with headers, end (beyond end index)",
modelStrings: []string{"a", "b", "c"},
modelStrings: []mystring{"a", "b", "c"},
nonModelIndices: []int{1, 3},
startIdx: 2,
endIdx: 7,
@ -108,7 +115,7 @@ func TestListRenderer_renderLines(t *testing.T) {
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
viewModel := NewListViewModel[string](func() []string { return s.modelStrings })
viewModel := NewListViewModel[mystring](func() []mystring { return s.modelStrings })
var getNonModelItems func() []*NonModelItem
if s.nonModelIndices != nil {
getNonModelItems = func() []*NonModelItem {
@ -124,7 +131,7 @@ func TestListRenderer_renderLines(t *testing.T) {
list: viewModel,
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
return lo.Map(s.modelStrings[startIdx:endIdx],
func(s string, _ int) []string { return []string{s} })
func(s mystring, _ int) []string { return []string{string(s)} })
},
getNonModelItems: getNonModelItems,
}
@ -138,6 +145,12 @@ func TestListRenderer_renderLines(t *testing.T) {
}
}
type myint int
func (self myint) ID() string {
return fmt.Sprint(int(self))
}
func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
scenarios := []struct {
name string
@ -222,8 +235,8 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
assert.Equal(t, len(s.modelIndices), len(s.expectedViewIndices))
assert.Equal(t, len(s.viewIndices), len(s.expectedModelIndices))
modelInts := lo.Range(s.numModelItems)
viewModel := NewListViewModel[int](func() []int { return modelInts })
modelInts := lo.Map(lo.Range(s.numModelItems), func(i int, _ int) myint { return myint(i) })
viewModel := NewListViewModel[myint](func() []myint { return modelInts })
var getNonModelItems func() []*NonModelItem
if s.nonModelIndices != nil {
getNonModelItems = func() []*NonModelItem {
@ -236,7 +249,7 @@ func TestListRenderer_ModelIndexToViewIndex_and_back(t *testing.T) {
list: viewModel,
getDisplayStrings: func(startIdx int, endIdx int) [][]string {
return lo.Map(modelInts[startIdx:endIdx],
func(i int, _ int) []string { return []string{fmt.Sprint(i)} })
func(i myint, _ int) []string { return []string{fmt.Sprint(i)} })
},
getNonModelItems: getNonModelItems,
}

View File

@ -3,14 +3,19 @@ package context
import (
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type ListViewModel[T any] struct {
type HasID interface {
ID() string
}
type ListViewModel[T HasID] struct {
*traits.ListCursor
getModel func() []T
}
func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] {
func NewListViewModel[T HasID](getModel func() []T) *ListViewModel[T] {
self := &ListViewModel[T]{
getModel: getModel,
}
@ -32,6 +37,34 @@ func (self *ListViewModel[T]) GetSelected() T {
return self.getModel()[self.GetSelectedLineIdx()]
}
func (self *ListViewModel[T]) GetSelectedItemId() string {
if self.Len() == 0 {
return ""
}
return self.GetSelected().ID()
}
func (self *ListViewModel[T]) GetSelectedItems() ([]T, int, int) {
if self.Len() == 0 {
return nil, -1, -1
}
startIdx, endIdx := self.GetSelectionRange()
return self.getModel()[startIdx : endIdx+1], startIdx, endIdx
}
func (self *ListViewModel[T]) GetSelectedItemIds() ([]string, int, int) {
selectedItems, startIdx, endIdx := self.GetSelectedItems()
ids := lo.Map(selectedItems, func(item T, _ int) string {
return item.ID()
})
return ids, startIdx, endIdx
}
func (self *ListViewModel[T]) GetItems() []T {
return self.getModel()
}

View File

@ -92,15 +92,6 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
return ctx
}
func (self *LocalCommitsContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
type LocalCommitsViewModel struct {
*ListViewModel[*models.Commit]

View File

@ -45,16 +45,6 @@ func NewMenuContext(
}
}
// TODO: remove this thing.
func (self *MenuContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.Label
}
type MenuViewModel struct {
c *ContextCommon
menuItems []*types.MenuItem

View File

@ -59,15 +59,6 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
}
}
func (self *ReflogCommitsContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *ReflogCommitsContext) CanRebase() bool {
return false
}

View File

@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
type RemoteBranchesContext struct {
@ -53,15 +54,6 @@ func NewRemoteBranchesContext(
}
}
func (self *RemoteBranchesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *RemoteBranchesContext) GetSelectedRef() types.Ref {
remoteBranch := self.GetSelected()
if remoteBranch == nil {
@ -70,6 +62,16 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref {
return remoteBranch
}
func (self *RemoteBranchesContext) GetSelectedRefs() ([]types.Ref, int, int) {
items, startIdx, endIdx := self.GetSelectedItems()
refs := lo.Map(items, func(item *models.RemoteBranch, _ int) types.Ref {
return item
})
return refs, startIdx, endIdx
}
func (self *RemoteBranchesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()

View File

@ -47,15 +47,6 @@ func NewRemotesContext(c *ContextCommon) *RemotesContext {
}
}
func (self *RemotesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *RemotesContext) GetDiffTerminals() []string {
itemId := self.GetSelectedItemId()

View File

@ -49,15 +49,6 @@ func NewStashContext(
}
}
func (self *StashContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *StashContext) CanRebase() bool {
return false
}

View File

@ -175,15 +175,6 @@ func (self *SubCommitsViewModel) GetShowBranchHeads() bool {
return self.showBranchHeads
}
func (self *SubCommitsContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *SubCommitsContext) CanRebase() bool {
return false
}

View File

@ -43,12 +43,3 @@ func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
},
}
}
func (self *SubmodulesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}

View File

@ -63,15 +63,6 @@ func NewSuggestionsContext(
}
}
func (self *SuggestionsContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.Value
}
func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) {
self.State.Suggestions = suggestions
self.SetSelection(0)

View File

@ -52,15 +52,6 @@ func NewTagsContext(
}
}
func (self *TagsContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *TagsContext) GetSelectedRef() types.Ref {
tag := self.GetSelected()
if tag == nil {

View File

@ -58,12 +58,3 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
return ctx
}
func (self *WorkingTreeContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}

View File

@ -46,12 +46,3 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
},
}
}
func (self *WorktreesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}

View File

@ -16,6 +16,7 @@ type ContainsCommits interface {
types.Context
types.IListContext
GetSelected() *models.Commit
GetSelectedItems() ([]*models.Commit, int, int)
GetCommits() []*models.Commit
GetSelectedLineIdx() int
}
@ -36,6 +37,7 @@ func NewBasicCommitsController(c *ControllerCommon, context ContainsCommits) *Ba
c,
context,
context.GetSelected,
context.GetSelectedItems,
),
}
}

View File

@ -30,6 +30,7 @@ func NewBisectController(
c,
c.Contexts().LocalCommits,
c.Contexts().LocalCommits.GetSelected,
c.Contexts().LocalCommits.GetSelectedItems,
),
}
}

View File

@ -33,6 +33,7 @@ func NewBranchesController(
c,
c.Contexts().Branches,
c.Contexts().Branches.GetSelected,
c.Contexts().Branches.GetSelectedItems,
),
}
}

View File

@ -28,6 +28,7 @@ func NewCommitFilesController(
c,
c.Contexts().CommitFiles,
c.Contexts().CommitFiles.GetSelected,
c.Contexts().CommitFiles.GetSelectedItems,
),
}
}

View File

@ -28,6 +28,7 @@ func NewFilesController(
c,
c.Contexts().Files,
c.Contexts().Files.GetSelected,
c.Contexts().Files.GetSelectedItems,
),
}
}

View File

@ -28,6 +28,7 @@ func NewFilesRemoveController(
c,
c.Contexts().Files,
c.Contexts().Files.GetSelected,
c.Contexts().Files.GetSelectedItems,
),
}
}

View File

@ -25,6 +25,7 @@ func NewGitFlowController(
c,
c.Contexts().Branches,
c.Contexts().Branches.GetSelected,
c.Contexts().Branches.GetSelectedItems,
),
c: c,
}

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

@ -6,20 +6,23 @@ import "github.com/jesseduffield/lazygit/pkg/gui/types"
// ensuring a single item is selected, etc.
type ListControllerTrait[T comparable] struct {
c *ControllerCommon
context types.IListContext
getSelected func() T
c *ControllerCommon
context types.IListContext
getSelectedItem func() T
getSelectedItems func() ([]T, int, int)
}
func NewListControllerTrait[T comparable](
c *ControllerCommon,
context types.IListContext,
getSelected func() T,
getSelectedItems func() ([]T, int, int),
) *ListControllerTrait[T] {
return &ListControllerTrait[T]{
c: c,
context: context,
getSelected: getSelected,
c: c,
context: context,
getSelectedItem: getSelected,
getSelectedItems: getSelectedItems,
}
}
@ -47,7 +50,7 @@ func (self *ListControllerTrait[T]) singleItemSelected(callbacks ...func(T) *typ
}
var zeroValue T
item := self.getSelected()
item := self.getSelectedItem()
if item == zeroValue {
return &types.DisabledReason{Text: self.c.Tr.NoItemSelected}
}
@ -62,11 +65,46 @@ func (self *ListControllerTrait[T]) singleItemSelected(callbacks ...func(T) *typ
}
}
// Ensures that at least one item is selected.
func (self *ListControllerTrait[T]) itemRangeSelected(callbacks ...func([]T, int, int) *types.DisabledReason) func() *types.DisabledReason {
return func() *types.DisabledReason {
items, startIdx, endIdx := self.getSelectedItems()
if len(items) == 0 {
return &types.DisabledReason{Text: self.c.Tr.NoItemSelected}
}
for _, callback := range callbacks {
if reason := callback(items, startIdx, endIdx); reason != nil {
return reason
}
}
return nil
}
}
func (self *ListControllerTrait[T]) itemsSelected(callbacks ...func([]T) *types.DisabledReason) func() *types.DisabledReason { //nolint:unused
return func() *types.DisabledReason {
items, _, _ := self.getSelectedItems()
if len(items) == 0 {
return &types.DisabledReason{Text: self.c.Tr.NoItemSelected}
}
for _, callback := range callbacks {
if reason := callback(items); reason != nil {
return reason
}
}
return nil
}
}
// Passes the selected item to the callback. Used for handler functions.
func (self *ListControllerTrait[T]) withItem(callback func(T) error) func() error {
return func() error {
var zeroValue T
commit := self.getSelected()
commit := self.getSelectedItem()
if commit == zeroValue {
return self.c.ErrorMsg(self.c.Tr.NoItemSelected)
}
@ -75,12 +113,35 @@ func (self *ListControllerTrait[T]) withItem(callback func(T) error) func() erro
}
}
func (self *ListControllerTrait[T]) withItems(callback func([]T) error) func() error {
return func() error {
items, _, _ := self.getSelectedItems()
if len(items) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoItemSelected)
}
return callback(items)
}
}
// like withItems but also passes the start and end index of the selection
func (self *ListControllerTrait[T]) withItemsRange(callback func([]T, int, int) error) func() error {
return func() error {
items, startIdx, endIdx := self.getSelectedItems()
if len(items) == 0 {
return self.c.ErrorMsg(self.c.Tr.NoItemSelected)
}
return callback(items, startIdx, endIdx)
}
}
// Like withItem, but doesn't show an error message if no item is selected.
// Use this for click actions (it's a no-op to click empty space)
func (self *ListControllerTrait[T]) withItemGraceful(callback func(T) error) func() error {
return func() error {
var zeroValue T
commit := self.getSelected()
commit := self.getSelectedItem()
if commit == zeroValue {
return nil
}

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,106 @@ 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 !isChangeOfRebaseTodoAllowed(commit.Action) {
return &types.DisabledReason{Text: self.c.Tr.ChangingThisActionIsNotAllowed}
}
}
return nil
}
// These actions represent standard things you might want to do with a commit,
// as opposed to TODO actions like 'merge', 'update-ref', etc.
var standardActions = []todo.TodoCommand{
todo.Pick,
todo.Drop,
todo.Edit,
todo.Fixup,
todo.Squash,
todo.Reword,
}
func isChangeOfRebaseTodoAllowed(oldAction todo.TodoCommand) bool {
// Only allow updating a standard action, meaning we disallow
// updating a merge commit or update ref commit (until we decide what would be sensible
// to do in those cases)
return lo.Contains(standardActions, oldAction)
}
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

@ -22,6 +22,7 @@ func NewMenuController(
c,
c.Contexts().Menu,
c.Contexts().Menu.GetSelected,
c.Contexts().Menu.GetSelectedItems,
),
c: c,
}

View File

@ -23,6 +23,7 @@ func NewReflogCommitsController(
c,
c.Contexts().ReflogCommits,
c.Contexts().ReflogCommits.GetSelected,
c.Contexts().ReflogCommits.GetSelectedItems,
),
c: c,
}

View File

@ -26,6 +26,7 @@ func NewRemoteBranchesController(
c,
c.Contexts().RemoteBranches,
c.Contexts().RemoteBranches.GetSelected,
c.Contexts().RemoteBranches.GetSelectedItems,
),
c: c,
}

View File

@ -32,6 +32,7 @@ func NewRemotesController(
c,
c.Contexts().Remotes,
c.Contexts().Remotes.GetSelected,
c.Contexts().Remotes.GetSelectedItems,
),
c: c,
setRemoteBranches: setRemoteBranches,

View File

@ -24,6 +24,7 @@ func NewStashController(
c,
c.Contexts().Stash,
c.Contexts().Stash.GetSelected,
c.Contexts().Stash.GetSelectedItems,
),
c: c,
}

View File

@ -24,6 +24,7 @@ func NewSubCommitsController(
c,
c.Contexts().SubCommits,
c.Contexts().SubCommits.GetSelected,
c.Contexts().SubCommits.GetSelectedItems,
),
c: c,
}

View File

@ -29,6 +29,7 @@ func NewSubmodulesController(
c,
c.Contexts().Submodules,
c.Contexts().Submodules.GetSelected,
c.Contexts().Submodules.GetSelectedItems,
),
c: c,
}

View File

@ -22,6 +22,7 @@ func NewSuggestionsController(
c,
c.Contexts().Suggestions,
c.Contexts().Suggestions.GetSelected,
c.Contexts().Suggestions.GetSelectedItems,
),
c: c,
}

View File

@ -36,6 +36,9 @@ func NewSwitchToDiffFilesController(
c,
context,
context.GetSelectedRef,
func() ([]types.Ref, int, int) {
panic("Not implemented")
},
),
c: c,
context: context,

View File

@ -32,6 +32,9 @@ func NewSwitchToSubCommitsController(
c,
context,
context.GetSelectedRef,
func() ([]types.Ref, int, int) {
panic("Not implemented")
},
),
c: c,
context: context,

View File

@ -25,6 +25,7 @@ func NewTagsController(
c,
c.Contexts().Tags,
c.Contexts().Tags.GetSelected,
c.Contexts().Tags.GetSelectedItems,
),
c: c,
}

View File

@ -26,6 +26,7 @@ func NewWorktreeOptionsController(c *ControllerCommon, context CanViewWorktreeOp
c,
context,
context.GetSelectedItemId,
context.GetSelectedItemIds,
),
c: c,
context: context,

View File

@ -28,6 +28,7 @@ func NewWorktreesController(
c,
c.Contexts().Worktrees,
c.Contexts().Worktrees.GetSelected,
c.Contexts().Worktrees.GetSelectedItems,
),
c: c,
}

View File

@ -6,6 +6,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@ -69,6 +70,29 @@ func (self *CommitFileTreeViewModel) GetSelected() *CommitFileNode {
return self.Get(self.GetSelectedLineIdx())
}
func (self *CommitFileTreeViewModel) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *CommitFileTreeViewModel) GetSelectedItems() ([]*CommitFileNode, int, int) {
panic("Not implemented")
}
func (self *CommitFileTreeViewModel) GetSelectedItemIds() ([]string, int, int) {
selectedItems, startIdx, endIdx := self.GetSelectedItems()
ids := lo.Map(selectedItems, func(item *CommitFileNode, _ int) string {
return item.ID()
})
return ids, startIdx, endIdx
}
func (self *CommitFileTreeViewModel) GetSelectedFile() *models.CommitFile {
node := self.GetSelected()
if node == nil {

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
@ -43,6 +44,36 @@ func (self *FileTreeViewModel) GetSelected() *FileNode {
return self.Get(self.GetSelectedLineIdx())
}
func (self *FileTreeViewModel) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}
func (self *FileTreeViewModel) GetSelectedItems() ([]*FileNode, int, int) {
startIdx, endIdx := self.GetSelectionRange()
nodes := []*FileNode{}
for i := startIdx; i <= endIdx; i++ {
nodes = append(nodes, self.Get(i))
}
return nodes, startIdx, endIdx
}
func (self *FileTreeViewModel) GetSelectedItemIds() ([]string, int, int) {
selectedItems, startIdx, endIdx := self.GetSelectedItems()
ids := lo.Map(selectedItems, func(item *FileNode, _ int) string {
return item.ID()
})
return ids, startIdx, endIdx
}
func (self *FileTreeViewModel) GetSelectedFile() *models.File {
node := self.GetSelected()
if node == nil {

View File

@ -242,6 +242,12 @@ type MenuItem struct {
Section *MenuSection
}
// Defining this for the sake of conforming to the HasID interface, which is used
// in list contexts.
func (self *MenuItem) ID() string {
return self.Label
}
type Model struct {
CommitFiles []*models.CommitFile
Files []*models.File

View File

@ -136,6 +136,7 @@ type IListContext interface {
Context
GetSelectedItemId() string
GetSelectedItemIds() ([]string, int, int)
IsItemVisible(item HasUrn) bool
GetList() IList

View File

@ -6,3 +6,8 @@ type Suggestion struct {
// label is what is actually displayed so it can e.g. contain color
Label string
}
// Conforming to the HasID interface, which is needed for list contexts
func (self *Suggestion) ID() string {
return self.Value
}

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

@ -9,6 +9,8 @@ import (
)
type TextMatcher struct {
// If you add or change a field here, be sure to update the copy
// code in checkIsSelected()
*Matcher[string]
}
@ -95,8 +97,8 @@ func (self *TextMatcher) IsSelected() *TextMatcher {
// if the matcher has an `IsSelected` rule, it returns true, along with the matcher after that rule has been removed
func (self *TextMatcher) checkIsSelected() (bool, *TextMatcher) {
// copying into a new matcher in case we want to re-use the original later
newMatcher := &TextMatcher{}
*newMatcher = *self
newMatcher := &TextMatcher{Matcher: &Matcher[string]{}}
*newMatcher.Matcher = *self.Matcher
check := lo.ContainsBy(newMatcher.rules, func(rule matcherRule[string]) bool { return rule.name == IS_SELECTED_RULE_NAME })

View File

@ -211,29 +211,63 @@ func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) {
func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver {
view := self.getView()
var expectedStartIdx, expectedEndIdx int
foundSelectionStart := false
foundSelectionEnd := false
expectedSelectedLines := []string{}
for matcherIndex, matcher := range matchers {
lineIdx := matcherIndex + offset
checkIsSelected, matcher := matcher.checkIsSelected()
if checkIsSelected {
if foundSelectionEnd {
self.t.fail("The IsSelected matcher can only be used on a contiguous range of lines.")
}
if !foundSelectionStart {
expectedStartIdx = lineIdx
foundSelectionStart = true
}
expectedSelectedLines = append(expectedSelectedLines, matcher.name())
expectedEndIdx = lineIdx
} else if foundSelectionStart {
foundSelectionEnd = true
}
}
for matcherIndex, matcher := range matchers {
lineIdx := matcherIndex + offset
expectSelected, matcher := matcher.checkIsSelected()
self.t.matchString(matcher, fmt.Sprintf("Unexpected content in view '%s'.", view.Name()),
func() string {
return view.BufferLines()[lineIdx]
},
)
if checkIsSelected {
// If any of the matchers care about the selection, we need to
// assert on the selection for each matcher.
if foundSelectionStart {
self.t.assertWithRetries(func() (bool, string) {
startIdx, endIdx := self.getSelectedRange()
if lineIdx < startIdx || lineIdx > endIdx {
if startIdx == endIdx {
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), lineIdx, startIdx)
} else {
lines := self.getSelectedLines()
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected line %d to be in range %d to %d. Selected lines:\n---\n%s\n---\n\nExpected line: '%s'", view.Name(), lineIdx, startIdx, endIdx, strings.Join(lines, "\n"), matcher.name())
}
selected := lineIdx >= startIdx && lineIdx <= endIdx
if (selected && expectSelected) || (!selected && !expectSelected) {
return true, ""
}
return true, ""
lines := self.getSelectedLines()
return false, fmt.Sprintf(
"Unexpected selection in view '%s'. Expected %s to be selected but got %s.\nExpected selected lines:\n---\n%s\n---\n\nActual selected lines:\n---\n%s\n---\n",
view.Name(),
formatLineRange(startIdx, endIdx),
formatLineRange(expectedStartIdx, expectedEndIdx),
strings.Join(lines, "\n"),
strings.Join(expectedSelectedLines, "\n"),
)
})
}
}
@ -241,6 +275,14 @@ func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewD
return self
}
func formatLineRange(from int, to int) string {
if from == to {
return "line " + fmt.Sprintf("%d", from)
}
return "lines " + fmt.Sprintf("%d-%d", from, to)
}
// asserts on the content of the view i.e. the stuff within the view's frame.
func (self *ViewDriver) Content(matcher *TextMatcher) *ViewDriver {
self.t.matchString(matcher, fmt.Sprintf("%s: Unexpected content.", self.context),

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

@ -161,9 +161,11 @@ var tests = []*components.IntegrationTest{
interactive_rebase.EditTheConflCommit,
interactive_rebase.FixupFirstCommit,
interactive_rebase.FixupSecondCommit,
interactive_rebase.MidRebaseRangeSelect,
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 {