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

Implement squash, fixup, drop, and reword in terms of daemon

This commit is contained in:
Stefan Haller 2023-04-05 19:01:49 +02:00
parent ab25600ccb
commit b8fbe9756e
3 changed files with 104 additions and 66 deletions

View File

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

View File

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

View File

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