From 5dacbb6293bcff48649fd9fcae6af8e1b76d23ad Mon Sep 17 00:00:00 2001
From: Jesse Duffield
Date: Tue, 2 May 2023 19:05:42 +1000
Subject: [PATCH] merge master into refactor-better-encapsulation
---
README.md | 2 +-
docs/Config.md | 1 +
go.mod | 2 +-
go.sum | 4 +-
pkg/app/daemon/daemon.go | 318 ++++++++++++++----
pkg/app/daemon/rebase.go | 64 ++++
pkg/commands/git_commands/deps_test.go | 6 +
pkg/commands/git_commands/patch.go | 16 +-
pkg/commands/git_commands/rebase.go | 217 +++++-------
pkg/commands/git_commands/rebase_test.go | 3 +-
pkg/config/user_config.go | 78 ++---
pkg/gui/context.go | 10 +-
.../controllers/local_commits_controller.go | 14 +-
pkg/gui/gui_common.go | 4 +
pkg/gui/layout.go | 2 +-
pkg/gui/presentation/commits.go | 2 +-
pkg/gui/types/common.go | 1 +
.../interactive_rebase/amend_fixup_commit.go | 50 +++
.../drop_todo_commit_with_update_ref.go | 25 +-
...ommit_with_update_ref_show_branch_heads.go | 62 ++++
pkg/integration/tests/test_list.go | 2 +
pkg/utils/rebase_todo.go | 76 +++++
pkg/utils/rebase_todo_test.go | 108 ++++++
.../fsmiamoto/git-todo-parser/todo/parse.go | 10 +-
vendor/modules.txt | 2 +-
25 files changed, 792 insertions(+), 287 deletions(-)
create mode 100644 pkg/app/daemon/rebase.go
create mode 100644 pkg/integration/tests/interactive_rebase/amend_fixup_commit.go
create mode 100644 pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go
diff --git a/README.md b/README.md
index bb30e8aef..2e9483a54 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ A simple terminal UI for git commands, written in Go with the [gocui](https://gi
-















































+
















































## Elevator Pitch
diff --git a/docs/Config.md b/docs/Config.md
index 837230367..34e89c130 100644
--- a/docs/Config.md
+++ b/docs/Config.md
@@ -57,6 +57,7 @@ gui:
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
+ experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
showIcons: false
diff --git a/go.mod b/go.mod
index f86062a69..098d19c67 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
- github.com/fsmiamoto/git-todo-parser v0.0.4-0.20230403011024-617a5a7ce980
+ github.com/fsmiamoto/git-todo-parser v0.0.4
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.6.0
github.com/go-errors/errors v1.4.2
diff --git a/go.sum b/go.sum
index e6a83d627..dd4a8b81e 100644
--- a/go.sum
+++ b/go.sum
@@ -30,8 +30,8 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
-github.com/fsmiamoto/git-todo-parser v0.0.4-0.20230403011024-617a5a7ce980 h1:ay9aM+Ay9I4LJttUVF4EFVmeNUkS9/snYVFK6lwieVQ=
-github.com/fsmiamoto/git-todo-parser v0.0.4-0.20230403011024-617a5a7ce980/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
+github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
+github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
diff --git a/pkg/app/daemon/daemon.go b/pkg/app/daemon/daemon.go
index e040e79c2..adc287309 100644
--- a/pkg/app/daemon/daemon.go
+++ b/pkg/app/daemon/daemon.go
@@ -1,13 +1,17 @@
package daemon
import (
+ "encoding/json"
+ "fmt"
"log"
"os"
- "path/filepath"
- "strings"
+ "strconv"
+ "github.com/fsmiamoto/git-todo-parser/todo"
+ "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
- "github.com/jesseduffield/lazygit/pkg/env"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+ "github.com/samber/lo"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
@@ -15,38 +19,58 @@ import (
// 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=EXIT_IMMEDIATELY' to specify that we want to run lazygit
-// as a daemon which simply exits immediately. Any additional arguments we want
-// to pass to a daemon can be done via other env vars.
+// '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 string
+type DaemonKind int
const (
- InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
- ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
+ // for when we fail to parse the daemon kind
+ DaemonKindUnknown DaemonKind = iota
+
+ DaemonKindExitImmediately
+ DaemonKindCherryPick
+ DaemonKindMoveTodoUp
+ DaemonKindMoveTodoDown
+ DaemonKindInsertBreak
+ DaemonKindChangeTodoActions
+ DaemonKindMoveFixupCommitDown
)
const (
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
- RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
- // The `PrependLinesEnvKey` env variable is set to `true` to tell our daemon
- // 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"
+ // Contains json-encoded arguments to the daemon
+ DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
)
-type Daemon interface {
- Run() error
+func getInstruction() Instruction {
+ jsonData := os.Getenv(DaemonInstructionEnvKey)
+
+ mapping := map[DaemonKind]func(string) Instruction{
+ DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
+ DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
+ DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
+ DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
+ DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction],
+ DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction],
+ DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
+ }
+
+ return mapping[getDaemonKind()](jsonData)
}
func Handle(common *common.Common) {
- d := getDaemon(common)
- if d == nil {
+ if !InDaemonMode() {
return
}
- if err := d.Run(); err != nil {
+ instruction := getInstruction()
+
+ if err := instruction.run(common); err != nil {
log.Fatal(err)
}
@@ -54,73 +78,229 @@ func Handle(common *common.Common) {
}
func InDaemonMode() bool {
- return getDaemonKind() != ""
-}
-
-func getDaemon(common *common.Common) Daemon {
- switch getDaemonKind() {
- case InteractiveRebase:
- return &rebaseDaemon{c: common}
- case ExitImmediately:
- return &exitImmediatelyDaemon{c: common}
- }
-
- return nil
+ return getDaemonKind() != DaemonKindUnknown
}
func getDaemonKind() DaemonKind {
- return DaemonKind(os.Getenv(DaemonKindEnvKey))
-}
-
-type rebaseDaemon struct {
- c *common.Common
-}
-
-func (self *rebaseDaemon) Run() error {
- self.c.Log.Info("Lazygit invoked as interactive rebase demon")
- self.c.Log.Info("args: ", os.Args)
- path := os.Args[1]
-
- if strings.HasSuffix(path, "git-rebase-todo") {
- return self.writeTodoFile(path)
- } else if strings.HasSuffix(path, filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
- // if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
- // but in this case we don't need to edit it, so we'll just return
- } else {
- self.c.Log.Info("Lazygit demon did not match on any use cases")
+ intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
+ if err != nil {
+ return DaemonKindUnknown
}
+ return DaemonKind(intValue)
+}
+
+// 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 (self *rebaseDaemon) writeTodoFile(path string) error {
- todoContent := []byte(os.Getenv(RebaseTODOEnvKey))
+func NewExitImmediatelyInstruction() Instruction {
+ return &ExitImmediatelyInstruction{}
+}
- prependLines := os.Getenv(PrependLinesEnvKey) != ""
- if prependLines {
- existingContent, err := os.ReadFile(path)
- if err != nil {
- return err
+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 {
+ for _, c := range self.Changes {
+ if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction); err != nil {
+ return err
+ }
}
- todoContent = append(todoContent, existingContent...)
+ return nil
+ })
+}
+
+// Takes the sha of some commit, and the sha 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"
+type MoveFixupCommitDownInstruction struct {
+ OriginalSha string
+ FixupSha string
+}
+
+func NewMoveFixupCommitDownInstruction(originalSha string, fixupSha string) Instruction {
+ return &MoveFixupCommitDownInstruction{
+ OriginalSha: originalSha,
+ FixupSha: fixupSha,
}
-
- return os.WriteFile(path, todoContent, 0o644)
}
-func gitDir() string {
- dir := env.GetGitDirEnv()
- if dir == "" {
- return ".git"
+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.OriginalSha, self.FixupSha)
+ })
+}
+
+type MoveTodoUpInstruction struct {
+ Sha string
+}
+
+func NewMoveTodoUpInstruction(sha string) Instruction {
+ return &MoveTodoUpInstruction{
+ Sha: sha,
}
- return dir
}
-type exitImmediatelyDaemon struct {
- c *common.Common
+func (self *MoveTodoUpInstruction) Kind() DaemonKind {
+ return DaemonKindMoveTodoUp
}
-func (self *exitImmediatelyDaemon) Run() error {
- return nil
+func (self *MoveTodoUpInstruction) SerializedInstructions() string {
+ return serializeInstruction(self)
+}
+
+func (self *MoveTodoUpInstruction) run(common *common.Common) error {
+ return handleInteractiveRebase(common, func(path string) error {
+ return utils.MoveTodoUp(path, self.Sha, todo.Pick)
+ })
+}
+
+type MoveTodoDownInstruction struct {
+ Sha string
+}
+
+func NewMoveTodoDownInstruction(sha string) Instruction {
+ return &MoveTodoDownInstruction{
+ Sha: sha,
+ }
+}
+
+func (self *MoveTodoDownInstruction) Kind() DaemonKind {
+ return DaemonKindMoveTodoDown
+}
+
+func (self *MoveTodoDownInstruction) SerializedInstructions() string {
+ return serializeInstruction(self)
+}
+
+func (self *MoveTodoDownInstruction) run(common *common.Common) error {
+ return handleInteractiveRebase(common, func(path string) error {
+ return utils.MoveTodoDown(path, self.Sha, todo.Pick)
+ })
+}
+
+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"))
+ })
}
diff --git a/pkg/app/daemon/rebase.go b/pkg/app/daemon/rebase.go
new file mode 100644
index 000000000..8702f0f69
--- /dev/null
+++ b/pkg/app/daemon/rebase.go
@@ -0,0 +1,64 @@
+package daemon
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/fsmiamoto/git-todo-parser/todo"
+ "github.com/jesseduffield/generics/slices"
+ "github.com/jesseduffield/lazygit/pkg/commands/models"
+ "github.com/jesseduffield/lazygit/pkg/common"
+ "github.com/jesseduffield/lazygit/pkg/env"
+)
+
+type TodoLine struct {
+ Action string
+ Commit *models.Commit
+}
+
+func (self *TodoLine) ToString() string {
+ if self.Action == "break" {
+ return self.Action + "\n"
+ } else {
+ return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
+ }
+}
+
+func TodoLinesToString(todoLines []TodoLine) string {
+ lines := slices.Map(todoLines, func(todoLine TodoLine) string {
+ return todoLine.ToString()
+ })
+
+ return strings.Join(slices.Reverse(lines), "")
+}
+
+type ChangeTodoAction struct {
+ Sha string
+ NewAction todo.TodoCommand
+}
+
+func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
+ common.Log.Info("Lazygit invoked as interactive rebase demon")
+ common.Log.Info("args: ", os.Args)
+ path := os.Args[1]
+
+ if strings.HasSuffix(path, "git-rebase-todo") {
+ return f(path)
+ } else if strings.HasSuffix(path, filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
+ // if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
+ // but in this case we don't need to edit it, so we'll just return
+ } else {
+ common.Log.Info("Lazygit demon did not match on any use cases")
+ }
+
+ return nil
+}
+
+func gitDir() string {
+ dir := env.GetGitDirEnv()
+ if dir == "" {
+ return ".git"
+ }
+ return dir
+}
diff --git a/pkg/commands/git_commands/deps_test.go b/pkg/commands/git_commands/deps_test.go
index df2c96253..bcf36b168 100644
--- a/pkg/commands/git_commands/deps_test.go
+++ b/pkg/commands/git_commands/deps_test.go
@@ -15,6 +15,7 @@ import (
type commonDeps struct {
runner *oscommands.FakeCmdObjRunner
userConfig *config.UserConfig
+ gitVersion *GitVersion
gitConfig *git_config.FakeGitConfig
getenv func(string) string
removeFile func(string) error
@@ -48,6 +49,11 @@ func buildGitCommon(deps commonDeps) *GitCommon {
gitCommon.Common.UserConfig = config.GetDefaultConfig()
}
+ gitCommon.version = deps.gitVersion
+ if gitCommon.version == nil {
+ gitCommon.version = &GitVersion{2, 0, 0, ""}
+ }
+
gitConfig := deps.gitConfig
if gitConfig == nil {
gitConfig = git_config.NewFakeGitConfig(nil)
diff --git a/pkg/commands/git_commands/patch.go b/pkg/commands/git_commands/patch.go
index 8c956529c..06e5e0f67 100644
--- a/pkg/commands/git_commands/patch.go
+++ b/pkg/commands/git_commands/patch.go
@@ -3,7 +3,9 @@ package git_commands
import (
"fmt"
+ "github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors"
+ "github.com/jesseduffield/lazygit/pkg/app/daemon"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
@@ -103,18 +105,16 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
baseIndex := sourceCommitIdx + 1
- todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
- if i == sourceCommitIdx || i == destinationCommitIdx {
- return "edit"
- } else {
- return "pick"
- }
- })
+ changes := []daemon.ChangeTodoAction{
+ {Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
+ {Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
+ }
+ self.os.LogCommand(logTodoChanges(changes), false)
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: commits[baseIndex].Sha,
- todoLines: todoLines,
overrideEditor: true,
+ instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
if err != nil {
return err
diff --git a/pkg/commands/git_commands/rebase.go b/pkg/commands/git_commands/rebase.go
index 5828ffbaa..50c599a66 100644
--- a/pkg/commands/git_commands/rebase.go
+++ b/pkg/commands/git_commands/rebase.go
@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
+ "github.com/samber/lo"
)
type RebaseCommands struct {
@@ -55,14 +56,15 @@ 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
- }
+ changes := []daemon.ChangeTodoAction{{
+ Sha: commits[index].Sha,
+ NewAction: todo.Reword,
+ }}
+ self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
- baseShaOrRoot: sha,
- todoLines: todo,
+ baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
+ instruction: daemon.NewChangeTodoActionsInstruction(changes),
}), nil
}
@@ -99,96 +101,105 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
}
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
- // not appending to original slice so that we don't mutate it
- orderedCommits := append([]*models.Commit{}, commits[0:index]...)
- orderedCommits = append(orderedCommits, commits[index+1], commits[index])
-
- todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
-
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
+ sha := commits[index].Sha
+
+ self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
+
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
- todoLines: todoLines,
+ instruction: daemon.NewMoveTodoDownInstruction(sha),
overrideEditor: true,
}).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) MoveCommitUp(commits []*models.Commit, index int) error {
+ baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
+
+ sha := commits[index].Sha
+
+ self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
- baseShaOrRoot: sha,
- todoLines: todo,
+ baseShaOrRoot: baseShaOrRoot,
+ instruction: daemon.NewMoveTodoUpInstruction(sha),
overrideEditor: true,
}).Run()
}
-func (self *RebaseCommands) InteractiveRebaseBreakAfter(commits []*models.Commit, index int) error {
- todo, sha, err := self.BuildSingleActionTodo(commits, index-1, "pick")
- 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++
}
- todo = append(todo, TodoLine{Action: "break", Commit: nil})
+ baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
+
+ changes := []daemon.ChangeTodoAction{{
+ Sha: commits[index].Sha,
+ NewAction: action,
+ }}
+ self.os.LogCommand(logTodoChanges(changes), false)
+
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
- baseShaOrRoot: sha,
- todoLines: todo,
+ baseShaOrRoot: baseShaOrRoot,
overrideEditor: true,
+ instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
}
func (self *RebaseCommands) EditRebase(branchRef string) error {
- commands := []TodoLine{{Action: "break"}}
+ self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: branchRef,
- todoLines: commands,
- prepend: true,
+ instruction: daemon.NewInsertBreakInstruction(),
}).Run()
}
+func logTodoChanges(changes []daemon.ChangeTodoAction) string {
+ changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
+ return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
+ }), "\n")
+ return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
+}
+
type PrepareInteractiveRebaseCommandOpts struct {
baseShaOrRoot string
- todoLines []TodoLine
+ instruction daemon.Instruction
overrideEditor bool
- prepend bool
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
- todo := self.buildTodo(opts.todoLines)
ex := oscommands.GetLazygitPath()
- prependLines := ""
- if opts.prepend {
- prependLines = "TRUE"
- }
debug := "FALSE"
if self.Debug {
debug = "TRUE"
}
- cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash %s", opts.baseShaOrRoot)
+ rebaseMergesArg := " --rebase-merges"
+ if self.version.IsOlderThan(2, 22, 0) {
+ rebaseMergesArg = ""
+ }
+ cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash%s %s",
+ rebaseMergesArg, opts.baseShaOrRoot)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
- if todo == "" {
- gitSequenceEditor = "true"
+
+ if opts.instruction != nil {
+ cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
} else {
- self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
+ gitSequenceEditor = "true"
}
cmdObj.AddEnvVars(
- daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
- daemon.RebaseTODOEnvKey+"="+todo,
- daemon.PrependLinesEnvKey+"="+prependLines,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
@@ -202,63 +213,31 @@ 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 {
+func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
+ commit := commits[commitIndex]
+
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
return err
}
- return self.SquashAllAboveFixupCommits(commit)
-}
-
-// 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 {
- fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
- todos, err := utils.ReadRebaseTodoFile(fileName)
+ // Get the sha of the commit we just created
+ fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
if err != nil {
return err
}
- 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 == commit.Action && t.Commit == commit.Sha {
- t.Command = action
- return utils.WriteRebaseTodoFile(fileName, todos)
- }
- }
+ return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
+ baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
+ overrideEditor: true,
+ instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
+ }).Run()
+}
- // Should never get here
- return fmt.Errorf("Todo %s not found in git-rebase-todo", commit.Sha)
+// 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 {
+ return utils.EditRebaseTodo(
+ filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action)
}
// MoveTodoDown moves a rebase todo item down by one position
@@ -304,15 +283,16 @@ 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
- }
+ changes := []daemon.ChangeTodoAction{{
+ Sha: commits[commitIndex].Sha,
+ NewAction: todo.Edit,
+ }}
+ self.os.LogCommand(logTodoChanges(changes), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
- baseShaOrRoot: sha,
- todoLines: todo,
+ baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
+ instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
}
@@ -359,15 +339,16 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
}
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
+ instruction := daemon.NewExitImmediatelyInstruction()
lazyGitPath := oscommands.GetLazygitPath()
return cmdObj.
AddEnvVars(
- daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
"GIT_EDITOR="+lazyGitPath,
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
+ AddEnvVars(daemon.ToEnvVars(instruction)...).
Run()
}
@@ -401,47 +382,17 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
- todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
+ commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
+ return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
+ })
+ self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: "HEAD",
- todoLines: todoLines,
+ instruction: daemon.NewCherryPickCommitsInstruction(commits),
}).Run()
}
-func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
- lines := slices.Map(todoLines, func(todoLine TodoLine) string {
- return todoLine.ToString()
- })
-
- return strings.Join(slices.Reverse(lines), "")
-}
-
-func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
- return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
- return TodoLine{Action: f(commit, i), Commit: commit}
- })
-}
-
-func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
- return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
- return action
- })
-}
-
-type TodoLine struct {
- Action string
- Commit *models.Commit
-}
-
-func (self *TodoLine) ToString() string {
- if self.Action == "break" {
- return self.Action + "\n"
- } else {
- return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
- }
-}
-
// we can't start an interactive rebase from the first commit without passing the
// '--root' arg
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
diff --git a/pkg/commands/git_commands/rebase_test.go b/pkg/commands/git_commands/rebase_test.go
index b2cd88323..1ef22ff5d 100644
--- a/pkg/commands/git_commands/rebase_test.go
+++ b/pkg/commands/git_commands/rebase_test.go
@@ -2,6 +2,7 @@ package git_commands
import (
"regexp"
+ "strconv"
"testing"
"github.com/go-errors/errors"
@@ -63,7 +64,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
`^GIT_SEQUENCE_EDITOR=.*$`,
- "^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
+ "^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
} {
regexStr := regexStr
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go
index fb36bb7ea..82361ddbd 100644
--- a/pkg/config/user_config.go
+++ b/pkg/config/user_config.go
@@ -27,32 +27,33 @@ type RefresherConfig struct {
}
type GuiConfig struct {
- AuthorColors map[string]string `yaml:"authorColors"`
- BranchColors map[string]string `yaml:"branchColors"`
- ScrollHeight int `yaml:"scrollHeight"`
- ScrollPastBottom bool `yaml:"scrollPastBottom"`
- MouseEvents bool `yaml:"mouseEvents"`
- SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
- SkipStashWarning bool `yaml:"skipStashWarning"`
- SidePanelWidth float64 `yaml:"sidePanelWidth"`
- ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
- MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
- Language string `yaml:"language"`
- TimeFormat string `yaml:"timeFormat"`
- Theme ThemeConfig `yaml:"theme"`
- CommitLength CommitLengthConfig `yaml:"commitLength"`
- SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
- ShowListFooter bool `yaml:"showListFooter"`
- ShowFileTree bool `yaml:"showFileTree"`
- ShowRandomTip bool `yaml:"showRandomTip"`
- ShowCommandLog bool `yaml:"showCommandLog"`
- ShowBottomLine bool `yaml:"showBottomLine"`
- ShowIcons bool `yaml:"showIcons"`
- CommandLogSize int `yaml:"commandLogSize"`
- SplitDiff string `yaml:"splitDiff"`
- SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
- WindowSize string `yaml:"windowSize"`
- Border string `yaml:"border"`
+ AuthorColors map[string]string `yaml:"authorColors"`
+ BranchColors map[string]string `yaml:"branchColors"`
+ ScrollHeight int `yaml:"scrollHeight"`
+ ScrollPastBottom bool `yaml:"scrollPastBottom"`
+ MouseEvents bool `yaml:"mouseEvents"`
+ SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
+ SkipStashWarning bool `yaml:"skipStashWarning"`
+ SidePanelWidth float64 `yaml:"sidePanelWidth"`
+ ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
+ MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
+ Language string `yaml:"language"`
+ TimeFormat string `yaml:"timeFormat"`
+ Theme ThemeConfig `yaml:"theme"`
+ CommitLength CommitLengthConfig `yaml:"commitLength"`
+ SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
+ ShowListFooter bool `yaml:"showListFooter"`
+ ShowFileTree bool `yaml:"showFileTree"`
+ ShowRandomTip bool `yaml:"showRandomTip"`
+ ShowCommandLog bool `yaml:"showCommandLog"`
+ ShowBottomLine bool `yaml:"showBottomLine"`
+ ShowIcons bool `yaml:"showIcons"`
+ ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
+ CommandLogSize int `yaml:"commandLogSize"`
+ SplitDiff string `yaml:"splitDiff"`
+ SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
+ WindowSize string `yaml:"windowSize"`
+ Border string `yaml:"border"`
}
type ThemeConfig struct {
@@ -408,18 +409,19 @@ func GetDefaultConfig() *UserConfig {
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
- CommitLength: CommitLengthConfig{Show: true},
- SkipNoStagedFilesWarning: false,
- ShowListFooter: true,
- ShowCommandLog: true,
- ShowBottomLine: true,
- ShowFileTree: true,
- ShowRandomTip: true,
- ShowIcons: false,
- CommandLogSize: 8,
- SplitDiff: "auto",
- SkipRewordInEditorWarning: false,
- Border: "single",
+ CommitLength: CommitLengthConfig{Show: true},
+ SkipNoStagedFilesWarning: false,
+ ShowListFooter: true,
+ ShowCommandLog: true,
+ ShowBottomLine: true,
+ ShowFileTree: true,
+ ShowRandomTip: true,
+ ShowIcons: false,
+ ExperimentalShowBranchHeads: false,
+ CommandLogSize: 8,
+ SplitDiff: "auto",
+ SkipRewordInEditorWarning: false,
+ Border: "single",
},
Git: GitConfig{
Paging: PagingConfig{
diff --git a/pkg/gui/context.go b/pkg/gui/context.go
index 252815e18..d5a7f35a2 100644
--- a/pkg/gui/context.go
+++ b/pkg/gui/context.go
@@ -53,7 +53,7 @@ func (self *ContextMgr) Replace(c types.Context) error {
defer self.Unlock()
- return self.activateContext(c, types.OnFocusOpts{})
+ return self.ActivateContext(c, types.OnFocusOpts{})
}
func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) error {
@@ -83,7 +83,7 @@ func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) error {
return nil
}
- return self.activateContext(contextToActivate, singleOpts)
+ return self.ActivateContext(contextToActivate, singleOpts)
}
// Adjusts the context stack based on the context that's being pushed and
@@ -162,7 +162,7 @@ func (self *ContextMgr) Pop() error {
return err
}
- return self.activateContext(newContext, types.OnFocusOpts{})
+ return self.ActivateContext(newContext, types.OnFocusOpts{})
}
func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
@@ -192,7 +192,7 @@ func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
}
// activate the item at the top of the stack
- return self.activateContext(contextToActivate, types.OnFocusOpts{})
+ return self.ActivateContext(contextToActivate, types.OnFocusOpts{})
}
func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
@@ -218,7 +218,7 @@ func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLos
return nil
}
-func (self *ContextMgr) activateContext(c types.Context, opts types.OnFocusOpts) error {
+func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts) error {
viewName := c.GetViewName()
v, err := self.gui.c.GocuiGui().View(viewName)
if err != nil {
diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go
index 7ac6e8671..d3dbff9d4 100644
--- a/pkg/gui/controllers/local_commits_controller.go
+++ b/pkg/gui/controllers/local_commits_controller.go
@@ -217,7 +217,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)
})
},
})
@@ -242,7 +242,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)
})
},
})
@@ -338,7 +338,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)
})
},
})
@@ -355,7 +355,7 @@ func (self *LocalCommitsController) edit(commit *models.Commit) error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.EditCommit)
- err := self.c.Git().Rebase.InteractiveRebaseBreakAfter(self.c.Model().Commits, self.context().GetSelectedLineIdx())
+ err := self.c.Git().Rebase.EditRebase(commit.Sha)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
@@ -374,7 +374,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.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}
@@ -494,7 +494,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
- err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index-1)
+ err := self.c.Git().Rebase.MoveCommitUp(self.c.Model().Commits, index)
if err == nil {
self.context().MoveSelectedLine(-1)
}
@@ -520,7 +520,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
- err := self.c.Git().Rebase.AmendTo(commit)
+ err := self.c.Git().Rebase.AmendTo(self.c.Model().Commits, self.context().GetView().SelectedLineIdx())
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
},
diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go
index f5f977272..dfed29c44 100644
--- a/pkg/gui/gui_common.go
+++ b/pkg/gui/gui_common.go
@@ -76,6 +76,10 @@ func (self *guiCommon) Context() types.IContextMgr {
return self.gui.State.ContextMgr
}
+func (self *guiCommon) ActivateContext(context types.Context) error {
+ return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
+}
+
func (self *guiCommon) GetAppState() *config.AppState {
return self.gui.Config.GetAppState()
}
diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go
index 30177ac2f..5b3e6845f 100644
--- a/pkg/gui/layout.go
+++ b/pkg/gui/layout.go
@@ -182,7 +182,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
}
initialContext := gui.c.CurrentSideContext()
- if err := gui.c.PushContext(initialContext); err != nil {
+ if err := gui.c.ActivateContext(initialContext); err != nil {
return err
}
diff --git a/pkg/gui/presentation/commits.go b/pkg/gui/presentation/commits.go
index 7e16197b5..6b83456b9 100644
--- a/pkg/gui/presentation/commits.go
+++ b/pkg/gui/presentation/commits.go
@@ -277,7 +277,7 @@ func displayCommit(
} else {
if len(commit.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
- } else if commit.ExtraInfo != "" {
+ } else if common.UserConfig.Gui.ExperimentalShowBranchHeads && commit.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint("(*)") + " "
}
}
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index 90be93a32..3709d3c7b 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -67,6 +67,7 @@ type IGuiCommon interface {
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
Context() IContextMgr
+ ActivateContext(context Context) error
// enters search mode for the current view
OpenSearch()
diff --git a/pkg/integration/tests/interactive_rebase/amend_fixup_commit.go b/pkg/integration/tests/interactive_rebase/amend_fixup_commit.go
new file mode 100644
index 000000000..0c39c756b
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/amend_fixup_commit.go
@@ -0,0 +1,50 @@
+package interactive_rebase
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var AmendFixupCommit = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Amends a staged file to a fixup commit, and checks that other unrelated fixup commits are not auto-squashed.",
+ ExtraCmdArgs: "",
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.
+ CreateNCommits(1).
+ CreateFileAndAdd("first-fixup-file", "").Commit("fixup! commit 01").
+ CreateNCommitsStartingAt(2, 2).
+ CreateFileAndAdd("unrelated-fixup-file", "fixup 03").Commit("fixup! commit 03").
+ CreateFileAndAdd("fixup-file", "fixup 01")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Commits().
+ Focus().
+ Lines(
+ Contains("fixup! commit 03"),
+ Contains("commit 03"),
+ Contains("commit 02"),
+ Contains("fixup! commit 01"),
+ Contains("commit 01"),
+ ).
+ NavigateToLine(Contains("fixup! commit 01")).
+ Press(keys.Commits.AmendToCommit).
+ Tap(func() {
+ t.ExpectPopup().Confirmation().
+ Title(Equals("Amend Commit")).
+ Content(Contains("Are you sure you want to amend this commit with your staged files?")).
+ Confirm()
+ }).
+ Lines(
+ Contains("fixup! commit 03"),
+ Contains("commit 03"),
+ Contains("commit 02"),
+ Contains("fixup! commit 01").IsSelected(),
+ Contains("commit 01"),
+ )
+
+ t.Views().Main().
+ Content(Contains("fixup 01"))
+ },
+})
diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go
index c247f1743..ace5bed40 100644
--- a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go
+++ b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref.go
@@ -23,31 +23,22 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits().
Focus().
Lines(
- Contains("(*) commit 06").IsSelected(),
+ Contains("commit 06").IsSelected(),
Contains("commit 05"),
Contains("commit 04"),
- Contains("(*) commit 03"),
+ Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
).
- // Once "e" is fixed we can just hit "e", but for now we need to
- // manually do a command-line rebase
- // NavigateToLine(Contains("commit 01")).
- // Press(keys.Universal.Edit).
- Tap(func() {
- t.GlobalPress(keys.Universal.ExecuteCustomCommand)
- t.ExpectPopup().Prompt().
- Title(Equals("Custom Command:")).
- Type(`git -c core.editor="perl -i -lpe 'print \"break\" if $.==1'" rebase -i HEAD~5`).
- Confirm()
- }).
+ NavigateToLine(Contains("commit 01")).
+ Press(keys.Universal.Edit).
Focus().
Lines(
- Contains("pick").Contains("(*) commit 06"),
+ Contains("pick").Contains("commit 06"),
Contains("pick").Contains("commit 05"),
Contains("pick").Contains("commit 04"),
Contains("update-ref").Contains("master"),
- Contains("pick").Contains("(*) commit 03"),
+ Contains("pick").Contains("commit 03"),
Contains("pick").Contains("commit 02"),
Contains("<-- YOU ARE HERE --- commit 01"),
).
@@ -59,9 +50,9 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits().
IsFocused().
Lines(
- Contains("(*) commit 06"),
+ Contains("commit 06"),
Contains("commit 04"),
- Contains("(*) commit 03"),
+ Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
)
diff --git a/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go
new file mode 100644
index 000000000..cb42ce989
--- /dev/null
+++ b/pkg/integration/tests/interactive_rebase/drop_todo_commit_with_update_ref_show_branch_heads.go
@@ -0,0 +1,62 @@
+package interactive_rebase
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var DropTodoCommitWithUpdateRefShowBranchHeads = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file (with experimentalShowBranchHeads on)",
+ ExtraCmdArgs: "",
+ Skip: false,
+ GitVersion: From("2.38.0"),
+ SetupConfig: func(config *config.AppConfig) {
+ config.UserConfig.Gui.ExperimentalShowBranchHeads = true
+ },
+ SetupRepo: func(shell *Shell) {
+ shell.
+ CreateNCommits(3).
+ NewBranch("mybranch").
+ CreateNCommitsStartingAt(3, 4)
+
+ shell.SetConfig("rebase.updateRefs", "true")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Commits().
+ Focus().
+ Lines(
+ Contains("(*) commit 06").IsSelected(),
+ Contains("commit 05"),
+ Contains("commit 04"),
+ Contains("(*) commit 03"),
+ Contains("commit 02"),
+ Contains("commit 01"),
+ ).
+ NavigateToLine(Contains("commit 01")).
+ Press(keys.Universal.Edit).
+ Focus().
+ Lines(
+ Contains("pick").Contains("(*) commit 06"),
+ Contains("pick").Contains("commit 05"),
+ Contains("pick").Contains("commit 04"),
+ Contains("update-ref").Contains("master"),
+ Contains("pick").Contains("(*) commit 03"),
+ Contains("pick").Contains("commit 02"),
+ Contains("<-- YOU ARE HERE --- commit 01"),
+ ).
+ NavigateToLine(Contains("commit 05")).
+ Press(keys.Universal.Remove)
+
+ t.Common().ContinueRebase()
+
+ t.Views().Commits().
+ IsFocused().
+ Lines(
+ Contains("(*) commit 06"),
+ Contains("commit 04"),
+ Contains("(*) commit 03"),
+ Contains("commit 02"),
+ Contains("commit 01"),
+ )
+ },
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 8c1d75c6e..b2cb12e14 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -88,10 +88,12 @@ var tests = []*components.IntegrationTest{
filter_by_path.TypeFile,
interactive_rebase.AdvancedInteractiveRebase,
interactive_rebase.AmendFirstCommit,
+ interactive_rebase.AmendFixupCommit,
interactive_rebase.AmendHeadCommitDuringRebase,
interactive_rebase.AmendMerge,
interactive_rebase.AmendNonHeadCommitDuringRebase,
interactive_rebase.DropTodoCommitWithUpdateRef,
+ interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads,
interactive_rebase.EditFirstCommit,
interactive_rebase.EditNonTodoCommitDuringRebase,
interactive_rebase.FixupFirstCommit,
diff --git a/pkg/utils/rebase_todo.go b/pkg/utils/rebase_todo.go
index 0b6a6a40c..15c06b1c4 100644
--- a/pkg/utils/rebase_todo.go
+++ b/pkg/utils/rebase_todo.go
@@ -9,6 +9,29 @@ 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) error {
+ todos, err := ReadRebaseTodoFile(filePath)
+ if err != nil {
+ return err
+ }
+
+ 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)
+ }
+ }
+
+ // Should never get here
+ return fmt.Errorf("Todo %s not found in git-rebase-todo", sha)
+}
+
func equalShas(a, b string) bool {
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
}
@@ -40,6 +63,16 @@ func WriteRebaseTodoFile(fileName string, todos []todo.Todo) error {
return err
}
+func PrependStrToTodoFile(filePath string, linesToPrepend []byte) error {
+ existingContent, err := os.ReadFile(filePath)
+ if err != nil {
+ return err
+ }
+
+ linesToPrepend = append(linesToPrepend, existingContent...)
+ return os.WriteFile(filePath, linesToPrepend, 0o644)
+}
+
func MoveTodoDown(fileName string, sha string, action todo.TodoCommand) error {
todos, err := ReadRebaseTodoFile(fileName)
if err != nil {
@@ -101,6 +134,49 @@ func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.
return rearrangedTodos, nil
}
+func MoveFixupCommitDown(fileName string, originalSha string, fixupSha string) error {
+ todos, err := ReadRebaseTodoFile(fileName)
+ if err != nil {
+ return err
+ }
+
+ newTodos, err := moveFixupCommitDown(todos, originalSha, fixupSha)
+ if err != nil {
+ return err
+ }
+
+ return WriteRebaseTodoFile(fileName, newTodos)
+}
+
+func moveFixupCommitDown(todos []todo.Todo, originalSha string, fixupSha string) ([]todo.Todo, error) {
+ isOriginal := func(t todo.Todo) bool {
+ return t.Command == todo.Pick && equalShas(t.Commit, originalSha)
+ }
+
+ isFixup := func(t todo.Todo) bool {
+ return t.Command == todo.Pick && equalShas(t.Commit, fixupSha)
+ }
+
+ originalShaCount := lo.CountBy(todos, isOriginal)
+ if originalShaCount != 1 {
+ return nil, fmt.Errorf("Expected exactly one original SHA, found %d", originalShaCount)
+ }
+
+ fixupShaCount := lo.CountBy(todos, isFixup)
+ if fixupShaCount != 1 {
+ return nil, fmt.Errorf("Expected exactly one fixup SHA, found %d", fixupShaCount)
+ }
+
+ _, fixupIndex, _ := lo.FindIndexOf(todos, isFixup)
+ _, originalIndex, _ := lo.FindIndexOf(todos, isOriginal)
+
+ newTodos := MoveElement(todos, fixupIndex, originalIndex+1)
+
+ newTodos[originalIndex+1].Command = todo.Fixup
+
+ return newTodos, nil
+}
+
// We render a todo in the commits view if it's a commit or if it's an
// update-ref. We don't render label, reset, or comment lines.
func isRenderedTodo(t todo.Todo) bool {
diff --git a/pkg/utils/rebase_todo_test.go b/pkg/utils/rebase_todo_test.go
index a4b1a46d0..4f554e926 100644
--- a/pkg/utils/rebase_todo_test.go
+++ b/pkg/utils/rebase_todo_test.go
@@ -1,6 +1,7 @@
package utils
import (
+ "errors"
"testing"
"github.com/fsmiamoto/git-todo-parser/todo"
@@ -228,3 +229,110 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
)
}
}
+
+func TestRebaseCommands_moveFixupCommitDown(t *testing.T) {
+ scenarios := []struct {
+ name string
+ todos []todo.Todo
+ originalSha string
+ fixupSha string
+ expectedTodos []todo.Todo
+ expectedErr error
+ }{
+ {
+ name: "fixup commit is the last commit",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Pick, Commit: "fixup"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Fixup, Commit: "fixup"},
+ },
+ expectedErr: nil,
+ },
+ {
+ // TODO: is this something we actually want to support?
+ name: "fixup commit is separated from original commit",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Pick, Commit: "other"},
+ {Command: todo.Pick, Commit: "fixup"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Fixup, Commit: "fixup"},
+ {Command: todo.Pick, Commit: "other"},
+ },
+ expectedErr: nil,
+ },
+ {
+ name: "More original SHAs than expected",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Pick, Commit: "fixup"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: nil,
+ expectedErr: errors.New("Expected exactly one original SHA, found 2"),
+ },
+ {
+ name: "More fixup SHAs than expected",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ {Command: todo.Pick, Commit: "fixup"},
+ {Command: todo.Pick, Commit: "fixup"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: nil,
+ expectedErr: errors.New("Expected exactly one fixup SHA, found 2"),
+ },
+ {
+ name: "No fixup SHAs found",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "original"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: nil,
+ expectedErr: errors.New("Expected exactly one fixup SHA, found 0"),
+ },
+ {
+ name: "No original SHAs found",
+ todos: []todo.Todo{
+ {Command: todo.Pick, Commit: "fixup"},
+ },
+ originalSha: "original",
+ fixupSha: "fixup",
+ expectedTodos: nil,
+ expectedErr: errors.New("Expected exactly one original SHA, found 0"),
+ },
+ }
+
+ for _, scenario := range scenarios {
+ t.Run(scenario.name, func(t *testing.T) {
+ actualTodos, actualErr := moveFixupCommitDown(scenario.todos, scenario.originalSha, scenario.fixupSha)
+
+ if scenario.expectedErr == nil {
+ if !assert.NoError(t, actualErr) {
+ t.Errorf("Expected no error, got: %v", actualErr)
+ }
+ } else {
+ if !assert.EqualError(t, actualErr, scenario.expectedErr.Error()) {
+ t.Errorf("Expected err: %v, got: %v", scenario.expectedErr, actualErr)
+ }
+ }
+
+ if !assert.EqualValues(t, actualTodos, scenario.expectedTodos) {
+ t.Errorf("Expected todos: %v, got: %v", scenario.expectedTodos, actualTodos)
+ }
+ })
+ }
+}
diff --git a/vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go b/vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go
index 97c60db9d..ab3dd9ea9 100644
--- a/vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go
+++ b/vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go
@@ -56,9 +56,11 @@ func parseLine(line string) (Todo, error) {
fields := strings.Fields(line)
+ var commandLen int
for i := Pick; i < Comment; i++ {
if isCommand(i, fields[0]) {
todo.Command = i
+ commandLen = len(fields[0])
fields = fields[1:]
break
}
@@ -74,10 +76,14 @@ func parseLine(line string) (Todo, error) {
}
if todo.Command == Label || todo.Command == Reset {
- if len(fields) == 0 {
+ restOfLine := strings.TrimSpace(line[commandLen:])
+ if todo.Command == Reset && restOfLine == "[new root]" {
+ todo.Label = restOfLine
+ } else if len(fields) == 0 {
return todo, ErrMissingLabel
+ } else {
+ todo.Label = fields[0]
}
- todo.Label = fields[0]
return todo, nil
}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 12028e79c..fd06a3ec6 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -30,7 +30,7 @@ github.com/emirpasic/gods/utils
# github.com/fatih/color v1.9.0
## explicit; go 1.13
github.com/fatih/color
-# github.com/fsmiamoto/git-todo-parser v0.0.4-0.20230403011024-617a5a7ce980
+# github.com/fsmiamoto/git-todo-parser v0.0.4
## explicit; go 1.13
github.com/fsmiamoto/git-todo-parser/todo
# github.com/fsnotify/fsnotify v1.4.7