mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-25 12:24:47 +02:00
merge master into refactor-better-encapsulation
This commit is contained in:
parent
8d68ab41b6
commit
5dacbb6293
@ -57,6 +57,7 @@ gui:
|
|||||||
showFileTree: true # for rendering changes files in a tree format
|
showFileTree: true # for rendering changes files in a tree format
|
||||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||||
showRandomTip: true
|
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)
|
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
|
||||||
showCommandLog: true
|
showCommandLog: true
|
||||||
showIcons: false
|
showIcons: false
|
||||||
|
2
go.mod
2
go.mod
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/cli/safeexec v1.0.0
|
github.com/cli/safeexec v1.0.0
|
||||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||||
github.com/creack/pty v1.1.11
|
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/fsnotify/fsnotify v1.4.7
|
||||||
github.com/gdamore/tcell/v2 v2.6.0
|
github.com/gdamore/tcell/v2 v2.6.0
|
||||||
github.com/go-errors/errors v1.4.2
|
github.com/go-errors/errors v1.4.2
|
||||||
|
4
go.sum
4
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 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
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/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 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
|
||||||
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/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"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/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.
|
// 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
|
// 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
|
// 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
|
// 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
|
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
|
||||||
// as a daemon which simply exits immediately. Any additional arguments we want
|
// as a daemon which simply exits immediately.
|
||||||
// to pass to a daemon can be done via other env vars.
|
//
|
||||||
|
// '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 (
|
const (
|
||||||
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
|
// for when we fail to parse the daemon kind
|
||||||
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
|
DaemonKindUnknown DaemonKind = iota
|
||||||
|
|
||||||
|
DaemonKindExitImmediately
|
||||||
|
DaemonKindCherryPick
|
||||||
|
DaemonKindMoveTodoUp
|
||||||
|
DaemonKindMoveTodoDown
|
||||||
|
DaemonKindInsertBreak
|
||||||
|
DaemonKindChangeTodoActions
|
||||||
|
DaemonKindMoveFixupCommitDown
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
||||||
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
|
|
||||||
|
|
||||||
// The `PrependLinesEnvKey` env variable is set to `true` to tell our daemon
|
// Contains json-encoded arguments to the daemon
|
||||||
// to prepend the content of `RebaseTODOEnvKey` to the default `git-rebase-todo`
|
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
|
||||||
// file instead of using it as a replacement.
|
|
||||||
PrependLinesEnvKey string = "LAZYGIT_PREPEND_LINES"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Daemon interface {
|
func getInstruction() Instruction {
|
||||||
Run() error
|
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) {
|
func Handle(common *common.Common) {
|
||||||
d := getDaemon(common)
|
if !InDaemonMode() {
|
||||||
if d == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.Run(); err != nil {
|
instruction := getInstruction()
|
||||||
|
|
||||||
|
if err := instruction.run(common); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,73 +78,229 @@ func Handle(common *common.Common) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InDaemonMode() bool {
|
func InDaemonMode() bool {
|
||||||
return getDaemonKind() != ""
|
return getDaemonKind() != DaemonKindUnknown
|
||||||
}
|
|
||||||
|
|
||||||
func getDaemon(common *common.Common) Daemon {
|
|
||||||
switch getDaemonKind() {
|
|
||||||
case InteractiveRebase:
|
|
||||||
return &rebaseDaemon{c: common}
|
|
||||||
case ExitImmediately:
|
|
||||||
return &exitImmediatelyDaemon{c: common}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDaemonKind() DaemonKind {
|
func getDaemonKind() DaemonKind {
|
||||||
return DaemonKind(os.Getenv(DaemonKindEnvKey))
|
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
|
||||||
}
|
if err != nil {
|
||||||
|
return DaemonKindUnknown
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *rebaseDaemon) writeTodoFile(path string) error {
|
func NewExitImmediatelyInstruction() Instruction {
|
||||||
todoContent := []byte(os.Getenv(RebaseTODOEnvKey))
|
return &ExitImmediatelyInstruction{}
|
||||||
|
}
|
||||||
|
|
||||||
prependLines := os.Getenv(PrependLinesEnvKey) != ""
|
type CherryPickCommitsInstruction struct {
|
||||||
if prependLines {
|
Todo string
|
||||||
existingContent, err := os.ReadFile(path)
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
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 {
|
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
|
||||||
dir := env.GetGitDirEnv()
|
return DaemonKindMoveFixupCommitDown
|
||||||
if dir == "" {
|
}
|
||||||
return ".git"
|
|
||||||
|
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 {
|
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
|
||||||
c *common.Common
|
return DaemonKindMoveTodoUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *exitImmediatelyDaemon) Run() error {
|
func (self *MoveTodoUpInstruction) SerializedInstructions() string {
|
||||||
return nil
|
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"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
64
pkg/app/daemon/rebase.go
Normal file
64
pkg/app/daemon/rebase.go
Normal file
@ -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
|
||||||
|
}
|
@ -15,6 +15,7 @@ import (
|
|||||||
type commonDeps struct {
|
type commonDeps struct {
|
||||||
runner *oscommands.FakeCmdObjRunner
|
runner *oscommands.FakeCmdObjRunner
|
||||||
userConfig *config.UserConfig
|
userConfig *config.UserConfig
|
||||||
|
gitVersion *GitVersion
|
||||||
gitConfig *git_config.FakeGitConfig
|
gitConfig *git_config.FakeGitConfig
|
||||||
getenv func(string) string
|
getenv func(string) string
|
||||||
removeFile func(string) error
|
removeFile func(string) error
|
||||||
@ -48,6 +49,11 @@ func buildGitCommon(deps commonDeps) *GitCommon {
|
|||||||
gitCommon.Common.UserConfig = config.GetDefaultConfig()
|
gitCommon.Common.UserConfig = config.GetDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitCommon.version = deps.gitVersion
|
||||||
|
if gitCommon.version == nil {
|
||||||
|
gitCommon.version = &GitVersion{2, 0, 0, ""}
|
||||||
|
}
|
||||||
|
|
||||||
gitConfig := deps.gitConfig
|
gitConfig := deps.gitConfig
|
||||||
if gitConfig == nil {
|
if gitConfig == nil {
|
||||||
gitConfig = git_config.NewFakeGitConfig(nil)
|
gitConfig = git_config.NewFakeGitConfig(nil)
|
||||||
|
@ -3,7 +3,9 @@ package git_commands
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||||
"github.com/go-errors/errors"
|
"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/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||||
@ -103,18 +105,16 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
|||||||
|
|
||||||
baseIndex := sourceCommitIdx + 1
|
baseIndex := sourceCommitIdx + 1
|
||||||
|
|
||||||
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
changes := []daemon.ChangeTodoAction{
|
||||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
|
||||||
return "edit"
|
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
|
||||||
} else {
|
}
|
||||||
return "pick"
|
self.os.LogCommand(logTodoChanges(changes), false)
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: commits[baseIndex].Sha,
|
baseShaOrRoot: commits[baseIndex].Sha,
|
||||||
todoLines: todoLines,
|
|
||||||
overrideEditor: true,
|
overrideEditor: true,
|
||||||
|
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||||
}).Run()
|
}).Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RebaseCommands struct {
|
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) {
|
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
|
changes := []daemon.ChangeTodoAction{{
|
||||||
if err != nil {
|
Sha: commits[index].Sha,
|
||||||
return nil, err
|
NewAction: todo.Reword,
|
||||||
}
|
}}
|
||||||
|
self.os.LogCommand(logTodoChanges(changes), false)
|
||||||
|
|
||||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: sha,
|
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
|
||||||
todoLines: todo,
|
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||||
}), nil
|
}), 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 {
|
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)
|
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{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: baseShaOrRoot,
|
baseShaOrRoot: baseShaOrRoot,
|
||||||
todoLines: todoLines,
|
instruction: daemon.NewMoveTodoDownInstruction(sha),
|
||||||
overrideEditor: true,
|
overrideEditor: true,
|
||||||
}).Run()
|
}).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
|
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
|
||||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
|
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
|
||||||
if err != nil {
|
|
||||||
return err
|
sha := commits[index].Sha
|
||||||
}
|
|
||||||
|
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
|
||||||
|
|
||||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: sha,
|
baseShaOrRoot: baseShaOrRoot,
|
||||||
todoLines: todo,
|
instruction: daemon.NewMoveTodoUpInstruction(sha),
|
||||||
overrideEditor: true,
|
overrideEditor: true,
|
||||||
}).Run()
|
}).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RebaseCommands) InteractiveRebaseBreakAfter(commits []*models.Commit, index int) error {
|
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
|
||||||
todo, sha, err := self.BuildSingleActionTodo(commits, index-1, "pick")
|
baseIndex := index + 1
|
||||||
if err != nil {
|
if action == todo.Squash || action == todo.Fixup {
|
||||||
return err
|
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{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: sha,
|
baseShaOrRoot: baseShaOrRoot,
|
||||||
todoLines: todo,
|
|
||||||
overrideEditor: true,
|
overrideEditor: true,
|
||||||
|
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||||
}).Run()
|
}).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RebaseCommands) EditRebase(branchRef string) error {
|
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{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: branchRef,
|
baseShaOrRoot: branchRef,
|
||||||
todoLines: commands,
|
instruction: daemon.NewInsertBreakInstruction(),
|
||||||
prepend: true,
|
|
||||||
}).Run()
|
}).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 {
|
type PrepareInteractiveRebaseCommandOpts struct {
|
||||||
baseShaOrRoot string
|
baseShaOrRoot string
|
||||||
todoLines []TodoLine
|
instruction daemon.Instruction
|
||||||
overrideEditor bool
|
overrideEditor bool
|
||||||
prepend bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
// 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
|
// lazygit a todo string to write to the todo file
|
||||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||||
todo := self.buildTodo(opts.todoLines)
|
|
||||||
ex := oscommands.GetLazygitPath()
|
ex := oscommands.GetLazygitPath()
|
||||||
prependLines := ""
|
|
||||||
if opts.prepend {
|
|
||||||
prependLines = "TRUE"
|
|
||||||
}
|
|
||||||
|
|
||||||
debug := "FALSE"
|
debug := "FALSE"
|
||||||
if self.Debug {
|
if self.Debug {
|
||||||
debug = "TRUE"
|
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")
|
self.Log.WithField("command", cmdStr).Debug("RunCommand")
|
||||||
|
|
||||||
cmdObj := self.cmd.New(cmdStr)
|
cmdObj := self.cmd.New(cmdStr)
|
||||||
|
|
||||||
gitSequenceEditor := ex
|
gitSequenceEditor := ex
|
||||||
if todo == "" {
|
|
||||||
gitSequenceEditor = "true"
|
if opts.instruction != nil {
|
||||||
|
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
|
||||||
} else {
|
} else {
|
||||||
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
gitSequenceEditor = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdObj.AddEnvVars(
|
cmdObj.AddEnvVars(
|
||||||
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
|
|
||||||
daemon.RebaseTODOEnvKey+"="+todo,
|
|
||||||
daemon.PrependLinesEnvKey+"="+prependLines,
|
|
||||||
"DEBUG="+debug,
|
"DEBUG="+debug,
|
||||||
"LANG=en_US.UTF-8", // Force using EN as language
|
"LANG=en_US.UTF-8", // Force using EN as language
|
||||||
"LC_ALL=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
|
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
|
// 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 {
|
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.SquashAllAboveFixupCommits(commit)
|
// Get the sha of the commit we just created
|
||||||
}
|
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range todos {
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
t := &todos[i]
|
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||||
// Comparing just the sha is not enough; we need to compare both the
|
overrideEditor: true,
|
||||||
// action and the sha, as the sha could appear multiple times (e.g. in a
|
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
|
||||||
// pick and later in a merge)
|
}).Run()
|
||||||
if t.Command == commit.Action && t.Commit == commit.Sha {
|
}
|
||||||
t.Command = action
|
|
||||||
return utils.WriteRebaseTodoFile(fileName, todos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should never get here
|
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
|
||||||
return fmt.Errorf("Todo %s not found in git-rebase-todo", commit.Sha)
|
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
|
// 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)
|
return errors.New(self.Tr.DisabledForGPG)
|
||||||
}
|
}
|
||||||
|
|
||||||
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
|
changes := []daemon.ChangeTodoAction{{
|
||||||
if err != nil {
|
Sha: commits[commitIndex].Sha,
|
||||||
return err
|
NewAction: todo.Edit,
|
||||||
}
|
}}
|
||||||
|
self.os.LogCommand(logTodoChanges(changes), false)
|
||||||
|
|
||||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: sha,
|
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||||
todoLines: todo,
|
|
||||||
overrideEditor: true,
|
overrideEditor: true,
|
||||||
|
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||||
}).Run()
|
}).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,15 +339,16 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
||||||
|
instruction := daemon.NewExitImmediatelyInstruction()
|
||||||
lazyGitPath := oscommands.GetLazygitPath()
|
lazyGitPath := oscommands.GetLazygitPath()
|
||||||
return cmdObj.
|
return cmdObj.
|
||||||
AddEnvVars(
|
AddEnvVars(
|
||||||
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
|
|
||||||
"GIT_EDITOR="+lazyGitPath,
|
"GIT_EDITOR="+lazyGitPath,
|
||||||
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
|
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
|
||||||
"EDITOR="+lazyGitPath,
|
"EDITOR="+lazyGitPath,
|
||||||
"VISUAL="+lazyGitPath,
|
"VISUAL="+lazyGitPath,
|
||||||
).
|
).
|
||||||
|
AddEnvVars(daemon.ToEnvVars(instruction)...).
|
||||||
Run()
|
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
|
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||||
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
|
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{
|
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||||
baseShaOrRoot: "HEAD",
|
baseShaOrRoot: "HEAD",
|
||||||
todoLines: todoLines,
|
instruction: daemon.NewCherryPickCommitsInstruction(commits),
|
||||||
}).Run()
|
}).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
|
// we can't start an interactive rebase from the first commit without passing the
|
||||||
// '--root' arg
|
// '--root' arg
|
||||||
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
|
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
|
||||||
|
@ -2,6 +2,7 @@ package git_commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
@ -63,7 +64,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
|
|||||||
`^EDITOR=.*$`,
|
`^EDITOR=.*$`,
|
||||||
`^GIT_EDITOR=.*$`,
|
`^GIT_EDITOR=.*$`,
|
||||||
`^GIT_SEQUENCE_EDITOR=.*$`,
|
`^GIT_SEQUENCE_EDITOR=.*$`,
|
||||||
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
|
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
|
||||||
} {
|
} {
|
||||||
regexStr := regexStr
|
regexStr := regexStr
|
||||||
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
||||||
|
@ -27,32 +27,33 @@ type RefresherConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GuiConfig struct {
|
type GuiConfig struct {
|
||||||
AuthorColors map[string]string `yaml:"authorColors"`
|
AuthorColors map[string]string `yaml:"authorColors"`
|
||||||
BranchColors map[string]string `yaml:"branchColors"`
|
BranchColors map[string]string `yaml:"branchColors"`
|
||||||
ScrollHeight int `yaml:"scrollHeight"`
|
ScrollHeight int `yaml:"scrollHeight"`
|
||||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||||
MouseEvents bool `yaml:"mouseEvents"`
|
MouseEvents bool `yaml:"mouseEvents"`
|
||||||
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
|
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
|
||||||
SkipStashWarning bool `yaml:"skipStashWarning"`
|
SkipStashWarning bool `yaml:"skipStashWarning"`
|
||||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||||
Language string `yaml:"language"`
|
Language string `yaml:"language"`
|
||||||
TimeFormat string `yaml:"timeFormat"`
|
TimeFormat string `yaml:"timeFormat"`
|
||||||
Theme ThemeConfig `yaml:"theme"`
|
Theme ThemeConfig `yaml:"theme"`
|
||||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||||
ShowListFooter bool `yaml:"showListFooter"`
|
ShowListFooter bool `yaml:"showListFooter"`
|
||||||
ShowFileTree bool `yaml:"showFileTree"`
|
ShowFileTree bool `yaml:"showFileTree"`
|
||||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||||
ShowBottomLine bool `yaml:"showBottomLine"`
|
ShowBottomLine bool `yaml:"showBottomLine"`
|
||||||
ShowIcons bool `yaml:"showIcons"`
|
ShowIcons bool `yaml:"showIcons"`
|
||||||
CommandLogSize int `yaml:"commandLogSize"`
|
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
|
||||||
SplitDiff string `yaml:"splitDiff"`
|
CommandLogSize int `yaml:"commandLogSize"`
|
||||||
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
|
SplitDiff string `yaml:"splitDiff"`
|
||||||
WindowSize string `yaml:"windowSize"`
|
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
|
||||||
Border string `yaml:"border"`
|
WindowSize string `yaml:"windowSize"`
|
||||||
|
Border string `yaml:"border"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeConfig struct {
|
type ThemeConfig struct {
|
||||||
@ -408,18 +409,19 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
UnstagedChangesColor: []string{"red"},
|
UnstagedChangesColor: []string{"red"},
|
||||||
DefaultFgColor: []string{"default"},
|
DefaultFgColor: []string{"default"},
|
||||||
},
|
},
|
||||||
CommitLength: CommitLengthConfig{Show: true},
|
CommitLength: CommitLengthConfig{Show: true},
|
||||||
SkipNoStagedFilesWarning: false,
|
SkipNoStagedFilesWarning: false,
|
||||||
ShowListFooter: true,
|
ShowListFooter: true,
|
||||||
ShowCommandLog: true,
|
ShowCommandLog: true,
|
||||||
ShowBottomLine: true,
|
ShowBottomLine: true,
|
||||||
ShowFileTree: true,
|
ShowFileTree: true,
|
||||||
ShowRandomTip: true,
|
ShowRandomTip: true,
|
||||||
ShowIcons: false,
|
ShowIcons: false,
|
||||||
CommandLogSize: 8,
|
ExperimentalShowBranchHeads: false,
|
||||||
SplitDiff: "auto",
|
CommandLogSize: 8,
|
||||||
SkipRewordInEditorWarning: false,
|
SplitDiff: "auto",
|
||||||
Border: "single",
|
SkipRewordInEditorWarning: false,
|
||||||
|
Border: "single",
|
||||||
},
|
},
|
||||||
Git: GitConfig{
|
Git: GitConfig{
|
||||||
Paging: PagingConfig{
|
Paging: PagingConfig{
|
||||||
|
@ -53,7 +53,7 @@ func (self *ContextMgr) Replace(c types.Context) error {
|
|||||||
|
|
||||||
defer self.Unlock()
|
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 {
|
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 nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.activateContext(contextToActivate, singleOpts)
|
return self.ActivateContext(contextToActivate, singleOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjusts the context stack based on the context that's being pushed and
|
// 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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.activateContext(newContext, types.OnFocusOpts{})
|
return self.ActivateContext(newContext, types.OnFocusOpts{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
|
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
|
// 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 {
|
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
|
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()
|
viewName := c.GetViewName()
|
||||||
v, err := self.gui.c.GocuiGui().View(viewName)
|
v, err := self.gui.c.GocuiGui().View(viewName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -217,7 +217,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
|
|||||||
HandleConfirm: func() error {
|
HandleConfirm: func() error {
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
|
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown)
|
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 {
|
HandleConfirm: func() error {
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error {
|
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
|
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 {
|
HandleConfirm: func() error {
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error {
|
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.DropCommit)
|
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 {
|
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.EditCommit)
|
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)
|
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -374,7 +374,7 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error {
|
|||||||
return self.pullFiles()
|
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)
|
err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action)
|
||||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
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 {
|
return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitUp)
|
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 {
|
if err == nil {
|
||||||
self.context().MoveSelectedLine(-1)
|
self.context().MoveSelectedLine(-1)
|
||||||
}
|
}
|
||||||
@ -520,7 +520,7 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
|
|||||||
HandleConfirm: func() error {
|
HandleConfirm: func() error {
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
|
return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error {
|
||||||
self.c.LogAction(self.c.Tr.Actions.AmendCommit)
|
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)
|
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -76,6 +76,10 @@ func (self *guiCommon) Context() types.IContextMgr {
|
|||||||
return self.gui.State.ContextMgr
|
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 {
|
func (self *guiCommon) GetAppState() *config.AppState {
|
||||||
return self.gui.Config.GetAppState()
|
return self.gui.Config.GetAppState()
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialContext := gui.c.CurrentSideContext()
|
initialContext := gui.c.CurrentSideContext()
|
||||||
if err := gui.c.PushContext(initialContext); err != nil {
|
if err := gui.c.ActivateContext(initialContext); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ func displayCommit(
|
|||||||
} else {
|
} else {
|
||||||
if len(commit.Tags) > 0 {
|
if len(commit.Tags) > 0 {
|
||||||
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
|
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("(*)") + " "
|
tagString = style.FgMagenta.SetBold().Sprint("(*)") + " "
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
|
||||||
Context() IContextMgr
|
Context() IContextMgr
|
||||||
|
|
||||||
|
ActivateContext(context Context) error
|
||||||
// enters search mode for the current view
|
// enters search mode for the current view
|
||||||
OpenSearch()
|
OpenSearch()
|
||||||
|
|
||||||
|
@ -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"))
|
||||||
|
},
|
||||||
|
})
|
@ -23,31 +23,22 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
Focus().
|
Focus().
|
||||||
Lines(
|
Lines(
|
||||||
Contains("(*) commit 06").IsSelected(),
|
Contains("commit 06").IsSelected(),
|
||||||
Contains("commit 05"),
|
Contains("commit 05"),
|
||||||
Contains("commit 04"),
|
Contains("commit 04"),
|
||||||
Contains("(*) commit 03"),
|
Contains("commit 03"),
|
||||||
Contains("commit 02"),
|
Contains("commit 02"),
|
||||||
Contains("commit 01"),
|
Contains("commit 01"),
|
||||||
).
|
).
|
||||||
// Once "e" is fixed we can just hit "e", but for now we need to
|
NavigateToLine(Contains("commit 01")).
|
||||||
// manually do a command-line rebase
|
Press(keys.Universal.Edit).
|
||||||
// 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()
|
|
||||||
}).
|
|
||||||
Focus().
|
Focus().
|
||||||
Lines(
|
Lines(
|
||||||
Contains("pick").Contains("(*) commit 06"),
|
Contains("pick").Contains("commit 06"),
|
||||||
Contains("pick").Contains("commit 05"),
|
Contains("pick").Contains("commit 05"),
|
||||||
Contains("pick").Contains("commit 04"),
|
Contains("pick").Contains("commit 04"),
|
||||||
Contains("update-ref").Contains("master"),
|
Contains("update-ref").Contains("master"),
|
||||||
Contains("pick").Contains("(*) commit 03"),
|
Contains("pick").Contains("commit 03"),
|
||||||
Contains("pick").Contains("commit 02"),
|
Contains("pick").Contains("commit 02"),
|
||||||
Contains("<-- YOU ARE HERE --- commit 01"),
|
Contains("<-- YOU ARE HERE --- commit 01"),
|
||||||
).
|
).
|
||||||
@ -59,9 +50,9 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
|
|||||||
t.Views().Commits().
|
t.Views().Commits().
|
||||||
IsFocused().
|
IsFocused().
|
||||||
Lines(
|
Lines(
|
||||||
Contains("(*) commit 06"),
|
Contains("commit 06"),
|
||||||
Contains("commit 04"),
|
Contains("commit 04"),
|
||||||
Contains("(*) commit 03"),
|
Contains("commit 03"),
|
||||||
Contains("commit 02"),
|
Contains("commit 02"),
|
||||||
Contains("commit 01"),
|
Contains("commit 01"),
|
||||||
)
|
)
|
||||||
|
@ -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"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
@ -88,10 +88,12 @@ var tests = []*components.IntegrationTest{
|
|||||||
filter_by_path.TypeFile,
|
filter_by_path.TypeFile,
|
||||||
interactive_rebase.AdvancedInteractiveRebase,
|
interactive_rebase.AdvancedInteractiveRebase,
|
||||||
interactive_rebase.AmendFirstCommit,
|
interactive_rebase.AmendFirstCommit,
|
||||||
|
interactive_rebase.AmendFixupCommit,
|
||||||
interactive_rebase.AmendHeadCommitDuringRebase,
|
interactive_rebase.AmendHeadCommitDuringRebase,
|
||||||
interactive_rebase.AmendMerge,
|
interactive_rebase.AmendMerge,
|
||||||
interactive_rebase.AmendNonHeadCommitDuringRebase,
|
interactive_rebase.AmendNonHeadCommitDuringRebase,
|
||||||
interactive_rebase.DropTodoCommitWithUpdateRef,
|
interactive_rebase.DropTodoCommitWithUpdateRef,
|
||||||
|
interactive_rebase.DropTodoCommitWithUpdateRefShowBranchHeads,
|
||||||
interactive_rebase.EditFirstCommit,
|
interactive_rebase.EditFirstCommit,
|
||||||
interactive_rebase.EditNonTodoCommitDuringRebase,
|
interactive_rebase.EditNonTodoCommitDuringRebase,
|
||||||
interactive_rebase.FixupFirstCommit,
|
interactive_rebase.FixupFirstCommit,
|
||||||
|
@ -9,6 +9,29 @@ import (
|
|||||||
"github.com/samber/lo"
|
"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 {
|
func equalShas(a, b string) bool {
|
||||||
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
|
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
|
||||||
}
|
}
|
||||||
@ -40,6 +63,16 @@ func WriteRebaseTodoFile(fileName string, todos []todo.Todo) error {
|
|||||||
return err
|
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 {
|
func MoveTodoDown(fileName string, sha string, action todo.TodoCommand) error {
|
||||||
todos, err := ReadRebaseTodoFile(fileName)
|
todos, err := ReadRebaseTodoFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -101,6 +134,49 @@ func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.
|
|||||||
return rearrangedTodos, nil
|
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
|
// 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.
|
// update-ref. We don't render label, reset, or comment lines.
|
||||||
func isRenderedTodo(t todo.Todo) bool {
|
func isRenderedTodo(t todo.Todo) bool {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
10
vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go
generated
vendored
10
vendor/github.com/fsmiamoto/git-todo-parser/todo/parse.go
generated
vendored
@ -56,9 +56,11 @@ func parseLine(line string) (Todo, error) {
|
|||||||
|
|
||||||
fields := strings.Fields(line)
|
fields := strings.Fields(line)
|
||||||
|
|
||||||
|
var commandLen int
|
||||||
for i := Pick; i < Comment; i++ {
|
for i := Pick; i < Comment; i++ {
|
||||||
if isCommand(i, fields[0]) {
|
if isCommand(i, fields[0]) {
|
||||||
todo.Command = i
|
todo.Command = i
|
||||||
|
commandLen = len(fields[0])
|
||||||
fields = fields[1:]
|
fields = fields[1:]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -74,10 +76,14 @@ func parseLine(line string) (Todo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if todo.Command == Label || todo.Command == Reset {
|
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
|
return todo, ErrMissingLabel
|
||||||
|
} else {
|
||||||
|
todo.Label = fields[0]
|
||||||
}
|
}
|
||||||
todo.Label = fields[0]
|
|
||||||
return todo, nil
|
return todo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -30,7 +30,7 @@ github.com/emirpasic/gods/utils
|
|||||||
# github.com/fatih/color v1.9.0
|
# github.com/fatih/color v1.9.0
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/fatih/color
|
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
|
## explicit; go 1.13
|
||||||
github.com/fsmiamoto/git-todo-parser/todo
|
github.com/fsmiamoto/git-todo-parser/todo
|
||||||
# github.com/fsnotify/fsnotify v1.4.7
|
# github.com/fsnotify/fsnotify v1.4.7
|
||||||
|
Loading…
x
Reference in New Issue
Block a user