From e756511042bf65fdac330aec2917d53ed445db9e Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 23 Jul 2026 09:17:18 +0200 Subject: [PATCH] Move dragged commits in one rebase Let the todo-move primitives take a distance instead of hardcoding a single row, by iterating the one-row move in memory. Dropping a commit several rows away thus rewrites the todo file once and, outside of an interactive rebase, runs a single rebase rather than one per row. --- pkg/app/daemon/daemon.go | 20 +++++---- pkg/commands/git_commands/rebase.go | 40 ++++++++--------- .../controllers/local_commits_controller.go | 12 +----- pkg/utils/rebase_todo.go | 31 +++++++++---- pkg/utils/rebase_todo_test.go | 43 +++++++++++++++++++ 5 files changed, 99 insertions(+), 47 deletions(-) diff --git a/pkg/app/daemon/daemon.go b/pkg/app/daemon/daemon.go index df0e4bb49..0b33bc12b 100644 --- a/pkg/app/daemon/daemon.go +++ b/pkg/app/daemon/daemon.go @@ -263,12 +263,14 @@ func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error { } type MoveTodosUpInstruction struct { - Hashes []string + Hashes []string + Distance int } -func NewMoveTodosUpInstruction(hashes []string) Instruction { +func NewMoveTodosUpInstruction(hashes []string, distance int) Instruction { return &MoveTodosUpInstruction{ - Hashes: hashes, + Hashes: hashes, + Distance: distance, } } @@ -288,17 +290,19 @@ func (self *MoveTodosUpInstruction) run(common *common.Common) error { }) return handleInteractiveRebase(common, func(path string) error { - return utils.MoveTodosUp(path, todosToMove, false, getCommentChar()) + return utils.MoveTodos(path, todosToMove, false, -self.Distance, getCommentChar()) }) } type MoveTodosDownInstruction struct { - Hashes []string + Hashes []string + Distance int } -func NewMoveTodosDownInstruction(hashes []string) Instruction { +func NewMoveTodosDownInstruction(hashes []string, distance int) Instruction { return &MoveTodosDownInstruction{ - Hashes: hashes, + Hashes: hashes, + Distance: distance, } } @@ -318,7 +322,7 @@ func (self *MoveTodosDownInstruction) run(common *common.Common) error { }) return handleInteractiveRebase(common, func(path string) error { - return utils.MoveTodosDown(path, todosToMove, false, getCommentChar()) + return utils.MoveTodos(path, todosToMove, false, self.Distance, getCommentChar()) }) } diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 97d48a1a0..74278b18d 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -112,29 +112,30 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, start, end in } func (self *RebaseCommands) MoveCommitsDown(commits []*models.Commit, startIdx int, endIdx int) error { - baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+2) - - hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string { - return commit.Hash() - }) - - return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ - baseHashOrRoot: baseHashOrRoot, - instruction: daemon.NewMoveTodosDownInstruction(hashes), - overrideEditor: true, - }).Run() + return self.MoveCommits(commits, startIdx, endIdx, 1) } func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int, endIdx int) error { - baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+1) + return self.MoveCommits(commits, startIdx, endIdx, -1) +} + +func (self *RebaseCommands) MoveCommits(commits []*models.Commit, startIdx int, endIdx int, offset int) error { + baseHashOrRoot := getBaseHashOrRoot(commits, endIdx+max(offset, 0)+1) hashes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) string { return commit.Hash() }) + var instruction daemon.Instruction + if offset > 0 { + instruction = daemon.NewMoveTodosDownInstruction(hashes, offset) + } else { + instruction = daemon.NewMoveTodosUpInstruction(hashes, -offset) + } + return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ baseHashOrRoot: baseHashOrRoot, - instruction: daemon.NewMoveTodosUpInstruction(hashes), + instruction: instruction, overrideEditor: true, }).Run() } @@ -369,21 +370,20 @@ func (self *RebaseCommands) DeleteUpdateRefTodos(commits []*models.Commit) error } func (self *RebaseCommands) MoveTodosDown(commits []*models.Commit) error { - fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") - todosToMove := lo.Map(commits, func(commit *models.Commit, _ int) utils.Todo { - return todoFromCommit(commit) - }) - - return utils.MoveTodosDown(fileName, todosToMove, true, self.config.GetCoreCommentChar()) + return self.MoveTodos(commits, 1) } func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error { + return self.MoveTodos(commits, -1) +} + +func (self *RebaseCommands) MoveTodos(commits []*models.Commit, offset int) error { fileName := filepath.Join(self.repoPaths.WorktreeGitDirPath(), "rebase-merge/git-rebase-todo") todosToMove := lo.Map(commits, func(commit *models.Commit, _ int) utils.Todo { return todoFromCommit(commit) }) - return utils.MoveTodosUp(fileName, todosToMove, true, self.config.GetCoreCommentChar()) + return utils.MoveTodos(fileName, todosToMove, true, offset, self.config.GetCoreCommentChar()) } // SquashAllAboveFixupCommits squashes all fixup! commits above the given one diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index ab16a2a21..f092fb7df 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -743,13 +743,7 @@ func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, sta func (self *LocalCommitsController) move(selectedCommits []*models.Commit, startIdx int, endIdx int, offset int) error { if self.isRebasing() { - var err error - if offset > 0 { - err = self.c.Git().Rebase.MoveTodosDown(selectedCommits) - } else { - err = self.c.Git().Rebase.MoveTodosUp(selectedCommits) - } - if err != nil { + if err := self.c.Git().Rebase.MoveTodos(selectedCommits, offset); err != nil { return err } self.context().MoveSelection(offset) @@ -767,14 +761,12 @@ func (self *LocalCommitsController) move(selectedCommits []*models.Commit, start commits := self.c.Model().Commits return self.c.WithWaitingStatusBlockingInput(self.c.Tr.MovingStatus, func(gocui.Task) error { - var err error if offset > 0 { self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) - err = self.c.Git().Rebase.MoveCommitsDown(commits, startIdx, endIdx) } else { self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) - err = self.c.Git().Rebase.MoveCommitsUp(commits, startIdx, endIdx) } + err := self.c.Git().Rebase.MoveCommits(commits, startIdx, endIdx, offset) return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions( err, types.RefreshOptions{ BatchUIUpdates: true, diff --git a/pkg/utils/rebase_todo.go b/pkg/utils/rebase_todo.go index fe04cbc60..3a3577f80 100644 --- a/pkg/utils/rebase_todo.go +++ b/pkg/utils/rebase_todo.go @@ -144,27 +144,40 @@ func deleteTodos(todos []todo.Todo, todosToDelete []Todo) ([]todo.Todo, error) { } func MoveTodosDown(fileName string, todosToMove []Todo, isInRebase bool, commentChar byte) error { + return MoveTodos(fileName, todosToMove, isInRebase, 1, commentChar) +} + +func MoveTodosUp(fileName string, todosToMove []Todo, isInRebase bool, commentChar byte) error { + return MoveTodos(fileName, todosToMove, isInRebase, -1, commentChar) +} + +func MoveTodos(fileName string, todosToMove []Todo, isInRebase bool, offset int, commentChar byte) error { todos, err := ReadRebaseTodoFile(fileName, commentChar) if err != nil { return err } - rearrangedTodos, err := moveTodosDown(todos, todosToMove, isInRebase) + rearrangedTodos, err := moveTodos(todos, todosToMove, isInRebase, offset) if err != nil { return err } return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar) } -func MoveTodosUp(fileName string, todosToMove []Todo, isInRebase bool, commentChar byte) error { - todos, err := ReadRebaseTodoFile(fileName, commentChar) - if err != nil { - return err +func moveTodos(todos []todo.Todo, todosToMove []Todo, isInRebase bool, offset int) ([]todo.Todo, error) { + moveOneRow := moveTodosUp + if offset > 0 { + moveOneRow = moveTodosDown } - rearrangedTodos, err := moveTodosUp(todos, todosToMove, isInRebase) - if err != nil { - return err + + for range max(offset, -offset) { + var err error + todos, err = moveOneRow(todos, slices.Clone(todosToMove), isInRebase) + if err != nil { + return nil, err + } } - return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar) + + return todos, nil } func moveTodoDown(todos []todo.Todo, todoToMove Todo, isInRebase bool) ([]todo.Todo, error) { diff --git a/pkg/utils/rebase_todo_test.go b/pkg/utils/rebase_todo_test.go index 9daf7db01..a9ac1bba5 100644 --- a/pkg/utils/rebase_todo_test.go +++ b/pkg/utils/rebase_todo_test.go @@ -3,12 +3,55 @@ package utils import ( "errors" "fmt" + "slices" "testing" "github.com/stefanhaller/git-todo-parser/todo" "github.com/stretchr/testify/assert" ) +func TestMoveTodos(t *testing.T) { + todos := []todo.Todo{ + {Command: todo.Pick, Commit: "a"}, + {Command: todo.Pick, Commit: "b"}, + {Command: todo.Label, Label: "hidden"}, + {Command: todo.Pick, Commit: "c"}, + {Command: todo.Pick, Commit: "d"}, + {Command: todo.Pick, Commit: "e"}, + {Command: todo.Pick, Commit: "f"}, + } + + t.Run("moves a range up multiple rendered rows", func(t *testing.T) { + actual, err := moveTodos(slices.Clone(todos), []Todo{{Hash: "d"}, {Hash: "c"}}, false, -2) + + assert.NoError(t, err) + assert.Equal(t, []todo.Todo{ + {Command: todo.Pick, Commit: "a"}, + {Command: todo.Pick, Commit: "b"}, + {Command: todo.Label, Label: "hidden"}, + {Command: todo.Pick, Commit: "e"}, + {Command: todo.Pick, Commit: "f"}, + {Command: todo.Pick, Commit: "c"}, + {Command: todo.Pick, Commit: "d"}, + }, actual) + }) + + t.Run("moves a range down multiple rendered rows", func(t *testing.T) { + actual, err := moveTodos(slices.Clone(todos), []Todo{{Hash: "e"}, {Hash: "d"}}, false, 2) + + assert.NoError(t, err) + assert.Equal(t, []todo.Todo{ + {Command: todo.Pick, Commit: "a"}, + {Command: todo.Pick, Commit: "d"}, + {Command: todo.Pick, Commit: "e"}, + {Command: todo.Pick, Commit: "b"}, + {Command: todo.Label, Label: "hidden"}, + {Command: todo.Pick, Commit: "c"}, + {Command: todo.Pick, Commit: "f"}, + }, actual) + }) +} + func TestRebaseCommands_moveTodoDown(t *testing.T) { type scenario struct { testName string