diff --git a/pkg/app/daemon/daemon.go b/pkg/app/daemon/daemon.go index e040e79c2..c733c3bc1 100644 --- a/pkg/app/daemon/daemon.go +++ b/pkg/app/daemon/daemon.go @@ -1,13 +1,16 @@ package daemon import ( + "fmt" "log" "os" "path/filepath" "strings" + "github.com/fsmiamoto/git-todo-parser/todo" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/env" + "github.com/jesseduffield/lazygit/pkg/utils" ) // Sometimes lazygit will be invoked in daemon mode from a parent lazygit process. @@ -34,6 +37,15 @@ const ( // to prepend the content of `RebaseTODOEnvKey` to the default `git-rebase-todo` // file instead of using it as a replacement. PrependLinesEnvKey string = "LAZYGIT_PREPEND_LINES" + + // If this is set, it tells lazygit to read the original todo file, and + // change the action for one or more entries in it. The value of the variable + // will have one or more lines of the form "Sha1:newAction", e.g. + // a02b54e1b7e7e8dd8bc1958c11ef4ee4df459ea4:edit + // The existing action of the todo to be changed is expected to be "pick". + // + // If this is used, the value of RebaseTODOEnvKey must be empty. + ChangeTodoActionEnvKey string = "LAZYGIT_CHANGE_TODO_ACTION" ) type Daemon interface { @@ -94,19 +106,52 @@ func (self *rebaseDaemon) Run() error { } func (self *rebaseDaemon) writeTodoFile(path string) error { - todoContent := []byte(os.Getenv(RebaseTODOEnvKey)) + if changeTodoActionEnvValue := os.Getenv(ChangeTodoActionEnvKey); changeTodoActionEnvValue != "" { + return self.changeTodoAction(path, changeTodoActionEnvValue) + } else { + todoContent := []byte(os.Getenv(RebaseTODOEnvKey)) - prependLines := os.Getenv(PrependLinesEnvKey) != "" - if prependLines { - existingContent, err := os.ReadFile(path) - if err != nil { - return err + prependLines := os.Getenv(PrependLinesEnvKey) != "" + if prependLines { + existingContent, err := os.ReadFile(path) + if err != nil { + return err + } + + todoContent = append(todoContent, existingContent...) } - todoContent = append(todoContent, existingContent...) + return os.WriteFile(path, todoContent, 0o644) + } +} + +func (self *rebaseDaemon) changeTodoAction(path string, changeTodoActionEnvValue string) error { + lines := strings.Split(changeTodoActionEnvValue, "\n") + for _, line := range lines { + fields := strings.Split(line, ":") + if len(fields) != 2 { + return fmt.Errorf("Unexpected value for %s: %s", ChangeTodoActionEnvKey, changeTodoActionEnvValue) + } + sha, newAction := fields[0], self.actionFromString(fields[1]) + if int(newAction) == 0 { + return fmt.Errorf("Unknown action in %s", changeTodoActionEnvValue) + } + if err := utils.EditRebaseTodo(path, sha, todo.Pick, newAction); err != nil { + return err + } } - return os.WriteFile(path, todoContent, 0o644) + return nil +} + +func (self *rebaseDaemon) actionFromString(actionString string) todo.TodoCommand { + for t := todo.Pick; t < todo.Comment; t++ { + if t.String() == actionString { + return t + } + } + + return 0 } func gitDir() string { diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go index 31eaf1366..338917626 100644 --- a/pkg/commands/git_commands/rebase.go +++ b/pkg/commands/git_commands/rebase.go @@ -55,14 +55,14 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me } func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) { - todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword") - if err != nil { - return nil, err - } - return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ - baseShaOrRoot: sha, - todoLines: todo, + baseShaOrRoot: getBaseShaOrRoot(commits, index+1), + changeTodoActions: []ChangeTodoAction{ + { + sha: commits[index].Sha, + newAction: todo.Reword, + }, + }, }), nil } @@ -114,16 +114,21 @@ func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) }).Run() } -func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error { - todo, sha, err := self.BuildSingleActionTodo(commits, index, action) - if err != nil { - return err +func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error { + baseIndex := index + 1 + if action == todo.Squash || action == todo.Fixup { + baseIndex++ } + baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex) + return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ - baseShaOrRoot: sha, - todoLines: todo, + baseShaOrRoot: baseShaOrRoot, overrideEditor: true, + changeTodoActions: []ChangeTodoAction{{ + sha: commits[index].Sha, + newAction: action, + }}, }).Run() } @@ -136,11 +141,17 @@ func (self *RebaseCommands) EditRebase(branchRef string) error { }).Run() } +type ChangeTodoAction struct { + sha string + newAction todo.TodoCommand +} + type PrepareInteractiveRebaseCommandOpts struct { - baseShaOrRoot string - todoLines []TodoLine - overrideEditor bool - prepend bool + baseShaOrRoot string + todoLines []TodoLine + overrideEditor bool + prepend bool + changeTodoActions []ChangeTodoAction } // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase @@ -159,6 +170,14 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract debug = "TRUE" } + changeTodoValue := strings.Join(slices.Map(opts.changeTodoActions, func(c ChangeTodoAction) string { + return fmt.Sprintf("%s:%s", c.sha, c.newAction) + }), "\n") + + if todo != "" && changeTodoValue != "" { + panic("It's not allowed to pass both todoLines and changeActionOpts") + } + rebaseMergesArg := " --rebase-merges" if self.version.IsOlderThan(2, 22, 0) { rebaseMergesArg = "" @@ -170,16 +189,19 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract cmdObj := self.cmd.New(cmdStr) gitSequenceEditor := ex - if todo == "" { - gitSequenceEditor = "true" - } else { + if todo != "" { self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false) + } else if changeTodoValue != "" { + self.os.LogCommand(fmt.Sprintf("Changing TODO action: %s", changeTodoValue), false) + } else { + gitSequenceEditor = "true" } cmdObj.AddEnvVars( daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase), daemon.RebaseTODOEnvKey+"="+todo, daemon.PrependLinesEnvKey+"="+prependLines, + daemon.ChangeTodoActionEnvKey+"="+changeTodoValue, "DEBUG="+debug, "LANG=en_US.UTF-8", // Force using EN as language "LC_ALL=en_US.UTF-8", // Force using EN as language @@ -193,33 +215,6 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract return cmdObj } -// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which -// will have the given action applied to it. -func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) { - baseIndex := actionIndex + 1 - - if action == "squash" || action == "fixup" { - baseIndex++ - } - - todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string { - if i == actionIndex { - return action - } else if commit.IsMerge() { - // your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary! - // doing this means we don't need to worry about rebasing over merges which always causes problems. - // you typically shouldn't be doing rebases that pass over merge commits anyway. - return "drop" - } else { - return "pick" - } - }) - - baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex) - - return todoLines, baseShaOrRoot, nil -} - // AmendTo amends the given commit with whatever files are staged func (self *RebaseCommands) AmendTo(commit *models.Commit) error { if err := self.commit.CreateFixupCommit(commit.Sha); err != nil { @@ -278,15 +273,13 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co return errors.New(self.Tr.DisabledForGPG) } - todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit") - if err != nil { - return err - } - return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{ - baseShaOrRoot: sha, - todoLines: todo, + baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1), overrideEditor: true, + changeTodoActions: []ChangeTodoAction{{ + sha: commits[commitIndex].Sha, + newAction: todo.Edit, + }}, }).Run() } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 1d05385c9..a0adc8e5f 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -169,7 +169,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.SquashCommitDown) - return self.interactiveRebase("squash") + return self.interactiveRebase(todo.Squash) }) }, }) @@ -194,7 +194,7 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.FixupCommit) - return self.interactiveRebase("fixup") + return self.interactiveRebase(todo.Fixup) }) }, }) @@ -284,7 +284,7 @@ func (self *LocalCommitsController) drop(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.DropCommit) - return self.interactiveRebase("drop") + return self.interactiveRebase(todo.Drop) }) }, }) @@ -320,7 +320,7 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error { return self.pullFiles() } -func (self *LocalCommitsController) interactiveRebase(action string) error { +func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error { err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action) return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) }