mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-27 12:32:37 +02:00
For non-merge commits we change "pick" to "drop" when we delete them. We do this so that we can use the same code for dropping a commit no matter whether we are in an interactive rebase or not. (If we aren't, we could just as well delete the pick line from the todo list instead of setting it to "drop", but if we are, it is better to keep the line around so that the user can change it back to "pick" if they change their mind.) However, merge commits can't be changed to "drop", so we have to delete them from the todo file. We add a new daemon instruction that does this. We still don't allow deleting a merge commit from within an interactive rebase. The reason is that we don't show the "label" and "reset" todos in lazygit, so deleting a merge commit would leave the commits from the branch that is being merged in the list as "pick" commits, with no indication that they are going to be dropped because they are on a different branch, and the merge commit that would have brought them in is gone. This could be very confusing.
405 lines
11 KiB
Go
405 lines
11 KiB
Go
package daemon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
|
|
// We do this when git lets us supply a program to run within a git command.
|
|
// For example, if we want to ensure that a git command doesn't hang due to
|
|
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
|
|
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
|
|
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
|
|
// as a daemon which simply exits immediately.
|
|
//
|
|
// 'Daemon' is not the best name for this, because it's not a persistent background
|
|
// process, but it's close enough.
|
|
|
|
type DaemonKind int
|
|
|
|
const (
|
|
// for when we fail to parse the daemon kind
|
|
DaemonKindUnknown DaemonKind = iota
|
|
|
|
DaemonKindExitImmediately
|
|
DaemonKindRemoveUpdateRefsForCopiedBranch
|
|
DaemonKindCherryPick
|
|
DaemonKindMoveTodosUp
|
|
DaemonKindMoveTodosDown
|
|
DaemonKindInsertBreak
|
|
DaemonKindChangeTodoActions
|
|
DaemonKindDropMergeCommit
|
|
DaemonKindMoveFixupCommitDown
|
|
DaemonKindWriteRebaseTodo
|
|
)
|
|
|
|
const (
|
|
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
|
|
|
// Contains json-encoded arguments to the daemon
|
|
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
|
|
)
|
|
|
|
func getInstruction() Instruction {
|
|
jsonData := os.Getenv(DaemonInstructionEnvKey)
|
|
|
|
mapping := map[DaemonKind]func(string) Instruction{
|
|
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
|
|
DaemonKindRemoveUpdateRefsForCopiedBranch: deserializeInstruction[*RemoveUpdateRefsForCopiedBranchInstruction],
|
|
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
|
|
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
|
|
DaemonKindDropMergeCommit: deserializeInstruction[*DropMergeCommitInstruction],
|
|
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
|
|
DaemonKindMoveTodosUp: deserializeInstruction[*MoveTodosUpInstruction],
|
|
DaemonKindMoveTodosDown: deserializeInstruction[*MoveTodosDownInstruction],
|
|
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
|
|
DaemonKindWriteRebaseTodo: deserializeInstruction[*WriteRebaseTodoInstruction],
|
|
}
|
|
|
|
return mapping[getDaemonKind()](jsonData)
|
|
}
|
|
|
|
func Handle(common *common.Common) {
|
|
if !InDaemonMode() {
|
|
return
|
|
}
|
|
|
|
instruction := getInstruction()
|
|
|
|
if err := instruction.run(common); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
func InDaemonMode() bool {
|
|
return getDaemonKind() != DaemonKindUnknown
|
|
}
|
|
|
|
func getDaemonKind() DaemonKind {
|
|
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
|
|
if err != nil {
|
|
return DaemonKindUnknown
|
|
}
|
|
|
|
return DaemonKind(intValue)
|
|
}
|
|
|
|
func getCommentChar() byte {
|
|
cmd := exec.Command("git", "config", "--get", "--null", "core.commentChar")
|
|
if output, err := cmd.Output(); err == nil && len(output) == 2 {
|
|
return output[0]
|
|
}
|
|
|
|
return '#'
|
|
}
|
|
|
|
// An Instruction is a command to be run by lazygit in daemon mode.
|
|
// It is serialized to json and passed to lazygit via environment variables
|
|
type Instruction interface {
|
|
Kind() DaemonKind
|
|
SerializedInstructions() string
|
|
|
|
// runs the instruction
|
|
run(common *common.Common) error
|
|
}
|
|
|
|
func serializeInstruction[T any](instruction T) string {
|
|
jsonData, err := json.Marshal(instruction)
|
|
if err != nil {
|
|
// this should never happen
|
|
panic(err)
|
|
}
|
|
|
|
return string(jsonData)
|
|
}
|
|
|
|
func deserializeInstruction[T Instruction](jsonData string) Instruction {
|
|
var instruction T
|
|
err := json.Unmarshal([]byte(jsonData), &instruction)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return instruction
|
|
}
|
|
|
|
func ToEnvVars(instruction Instruction) []string {
|
|
return []string{
|
|
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
|
|
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
|
|
}
|
|
}
|
|
|
|
type ExitImmediatelyInstruction struct{}
|
|
|
|
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
|
|
return DaemonKindExitImmediately
|
|
}
|
|
|
|
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
|
|
return nil
|
|
}
|
|
|
|
func NewExitImmediatelyInstruction() Instruction {
|
|
return &ExitImmediatelyInstruction{}
|
|
}
|
|
|
|
type RemoveUpdateRefsForCopiedBranchInstruction struct{}
|
|
|
|
func (self *RemoveUpdateRefsForCopiedBranchInstruction) Kind() DaemonKind {
|
|
return DaemonKindRemoveUpdateRefsForCopiedBranch
|
|
}
|
|
|
|
func (self *RemoveUpdateRefsForCopiedBranchInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *RemoveUpdateRefsForCopiedBranchInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func NewRemoveUpdateRefsForCopiedBranchInstruction() Instruction {
|
|
return &RemoveUpdateRefsForCopiedBranchInstruction{}
|
|
}
|
|
|
|
type CherryPickCommitsInstruction struct {
|
|
Todo string
|
|
}
|
|
|
|
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
|
|
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
|
|
return TodoLine{
|
|
Action: "pick",
|
|
Commit: commit,
|
|
}
|
|
})
|
|
|
|
todo := TodoLinesToString(todoLines)
|
|
|
|
return &CherryPickCommitsInstruction{
|
|
Todo: todo,
|
|
}
|
|
}
|
|
|
|
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
|
|
return DaemonKindCherryPick
|
|
}
|
|
|
|
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
|
|
})
|
|
}
|
|
|
|
type ChangeTodoActionsInstruction struct {
|
|
Changes []ChangeTodoAction
|
|
}
|
|
|
|
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
|
|
return &ChangeTodoActionsInstruction{
|
|
Changes: changes,
|
|
}
|
|
}
|
|
|
|
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
|
|
return DaemonKindChangeTodoActions
|
|
}
|
|
|
|
func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
changes := lo.Map(self.Changes, func(c ChangeTodoAction, _ int) utils.TodoChange {
|
|
return utils.TodoChange{
|
|
Hash: c.Hash,
|
|
NewAction: c.NewAction,
|
|
}
|
|
})
|
|
|
|
return utils.EditRebaseTodo(path, changes, getCommentChar())
|
|
})
|
|
}
|
|
|
|
type DropMergeCommitInstruction struct {
|
|
Hash string
|
|
}
|
|
|
|
func NewDropMergeCommitInstruction(hash string) Instruction {
|
|
return &DropMergeCommitInstruction{
|
|
Hash: hash,
|
|
}
|
|
}
|
|
|
|
func (self *DropMergeCommitInstruction) Kind() DaemonKind {
|
|
return DaemonKindDropMergeCommit
|
|
}
|
|
|
|
func (self *DropMergeCommitInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *DropMergeCommitInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.DropMergeCommit(path, self.Hash, getCommentChar())
|
|
})
|
|
}
|
|
|
|
// Takes the hash of some commit, and the hash of a fixup commit that was created
|
|
// at the end of the branch, then moves the fixup commit down to right after the
|
|
// original commit, changing its type to "fixup" (only if ChangeToFixup is true)
|
|
type MoveFixupCommitDownInstruction struct {
|
|
OriginalHash string
|
|
FixupHash string
|
|
ChangeToFixup bool
|
|
}
|
|
|
|
func NewMoveFixupCommitDownInstruction(originalHash string, fixupHash string, changeToFixup bool) Instruction {
|
|
return &MoveFixupCommitDownInstruction{
|
|
OriginalHash: originalHash,
|
|
FixupHash: fixupHash,
|
|
ChangeToFixup: changeToFixup,
|
|
}
|
|
}
|
|
|
|
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
|
|
return DaemonKindMoveFixupCommitDown
|
|
}
|
|
|
|
func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.MoveFixupCommitDown(path, self.OriginalHash, self.FixupHash, self.ChangeToFixup, getCommentChar())
|
|
})
|
|
}
|
|
|
|
type MoveTodosUpInstruction struct {
|
|
Hashes []string
|
|
}
|
|
|
|
func NewMoveTodosUpInstruction(hashes []string) Instruction {
|
|
return &MoveTodosUpInstruction{
|
|
Hashes: hashes,
|
|
}
|
|
}
|
|
|
|
func (self *MoveTodosUpInstruction) Kind() DaemonKind {
|
|
return DaemonKindMoveTodosUp
|
|
}
|
|
|
|
func (self *MoveTodosUpInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *MoveTodosUpInstruction) run(common *common.Common) error {
|
|
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
|
|
return utils.Todo{
|
|
Hash: hash,
|
|
}
|
|
})
|
|
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.MoveTodosUp(path, todosToMove, getCommentChar())
|
|
})
|
|
}
|
|
|
|
type MoveTodosDownInstruction struct {
|
|
Hashes []string
|
|
}
|
|
|
|
func NewMoveTodosDownInstruction(hashes []string) Instruction {
|
|
return &MoveTodosDownInstruction{
|
|
Hashes: hashes,
|
|
}
|
|
}
|
|
|
|
func (self *MoveTodosDownInstruction) Kind() DaemonKind {
|
|
return DaemonKindMoveTodosDown
|
|
}
|
|
|
|
func (self *MoveTodosDownInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *MoveTodosDownInstruction) run(common *common.Common) error {
|
|
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
|
|
return utils.Todo{
|
|
Hash: hash,
|
|
}
|
|
})
|
|
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.MoveTodosDown(path, todosToMove, getCommentChar())
|
|
})
|
|
}
|
|
|
|
type InsertBreakInstruction struct{}
|
|
|
|
func NewInsertBreakInstruction() Instruction {
|
|
return &InsertBreakInstruction{}
|
|
}
|
|
|
|
func (self *InsertBreakInstruction) Kind() DaemonKind {
|
|
return DaemonKindInsertBreak
|
|
}
|
|
|
|
func (self *InsertBreakInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *InsertBreakInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return utils.PrependStrToTodoFile(path, []byte("break\n"))
|
|
})
|
|
}
|
|
|
|
type WriteRebaseTodoInstruction struct {
|
|
TodosFileContent []byte
|
|
}
|
|
|
|
func NewWriteRebaseTodoInstruction(todosFileContent []byte) Instruction {
|
|
return &WriteRebaseTodoInstruction{
|
|
TodosFileContent: todosFileContent,
|
|
}
|
|
}
|
|
|
|
func (self *WriteRebaseTodoInstruction) Kind() DaemonKind {
|
|
return DaemonKindWriteRebaseTodo
|
|
}
|
|
|
|
func (self *WriteRebaseTodoInstruction) SerializedInstructions() string {
|
|
return serializeInstruction(self)
|
|
}
|
|
|
|
func (self *WriteRebaseTodoInstruction) run(common *common.Common) error {
|
|
return handleInteractiveRebase(common, func(path string) error {
|
|
return os.WriteFile(path, self.TodosFileContent, 0o644)
|
|
})
|
|
}
|