mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-04 13:06:51 -04:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6c4346d48 | ||
|
|
ee9ae8f07f | ||
|
|
3ec416047f | ||
|
|
8ed29f2b7d | ||
|
|
9adbef40de | ||
|
|
0cd5257523 | ||
|
|
49da7b482d | ||
|
|
d675117289 | ||
|
|
e63858215e | ||
|
|
3fe4db9316 | ||
|
|
185bbf0c75 | ||
|
|
a8586ba57e | ||
|
|
dad7a70bf8 | ||
|
|
3791f0b2fa | ||
|
|
b8fbe9756e | ||
|
|
ab25600ccb | ||
|
|
d50c58b4c6 | ||
|
|
5645a662de | ||
|
|
a41218551d | ||
|
|
d210107caa | ||
|
|
826128a8e0 | ||
|
|
aec46942a8 | ||
|
|
aa70723e3a | ||
|
|
7db54a948a | ||
|
|
32556480da | ||
|
|
2553015029 | ||
|
|
6c010a788c | ||
|
|
b17c38befd | ||
|
|
79dc1d9052 | ||
|
|
28d2b1432b | ||
|
|
ed98e11fa0 | ||
|
|
07a22e69e7 | ||
|
|
bf3dd79b7a | ||
|
|
21072226d2 | ||
|
|
b09000194a | ||
|
|
3546ab8f21 | ||
|
|
9a13447b97 | ||
|
|
1efb565b22 | ||
|
|
08a445eb9d | ||
|
|
28501fbf77 | ||
|
|
8f1f712841 | ||
|
|
b82b6a2992 | ||
|
|
ddcd6be245 | ||
|
|
711be78811 | ||
|
|
3422b1e218 | ||
|
|
a3fdf91714 | ||
|
|
368f9c8cb3 | ||
|
|
e18b4a4cc3 | ||
|
|
46718e25ca | ||
|
|
d675eb6507 | ||
|
|
dc4e88f8a4 | ||
|
|
120dd1530a | ||
|
|
860a8d102b | ||
|
|
a304fed68c | ||
|
|
227b0b781c | ||
|
|
740474c10c | ||
|
|
a0d179b6dc | ||
|
|
c53c5e47ef | ||
|
|
188773511e | ||
|
|
62c5c32fbb | ||
|
|
981ba4c66c | ||
|
|
7f5465a27b | ||
|
|
275ed12486 | ||
|
|
6a340ec840 | ||
|
|
3535cd0f94 | ||
|
|
b1a56249f5 |
@@ -135,6 +135,13 @@ jobs:
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||
- name: Check Integration Test List
|
||||
# ensure our integration test list is up to date
|
||||
run: |
|
||||
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Integration test list not up to date. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
|
||||
shell: bash # needed so that we get "-o pipefail"
|
||||
- name: Check Filenames
|
||||
run: scripts/check_filenames.sh
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
+1
-2
@@ -171,7 +171,6 @@ keybinding:
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
increaseContextInDiffView: '}'
|
||||
@@ -474,7 +473,7 @@ services:
|
||||
Where:
|
||||
|
||||
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops`, `gitlab` or `gitea`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
||||
@@ -4,12 +4,12 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
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.2
|
||||
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
|
||||
|
||||
@@ -8,6 +8,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
|
||||
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
|
||||
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
@@ -28,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.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2/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=
|
||||
|
||||
+252
-52
@@ -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,33 +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"
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -49,58 +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))
|
||||
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
|
||||
if err != nil {
|
||||
return DaemonKindUnknown
|
||||
}
|
||||
|
||||
return DaemonKind(intValue)
|
||||
}
|
||||
|
||||
type rebaseDaemon struct {
|
||||
c *common.Common
|
||||
// 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 (self *rebaseDaemon) Run() error {
|
||||
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
self.c.Log.Info("args: ", os.Args)
|
||||
func serializeInstruction[T any](instruction T) string {
|
||||
jsonData, err := json.Marshal(instruction)
|
||||
if err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := os.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
|
||||
return err
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func deserializeInstruction[T Instruction](jsonData string) Instruction {
|
||||
var instruction T
|
||||
err := json.Unmarshal([]byte(jsonData), &instruction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return instruction
|
||||
}
|
||||
|
||||
func ToEnvVars(instruction Instruction) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
|
||||
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
|
||||
}
|
||||
}
|
||||
|
||||
type ExitImmediatelyInstruction struct{}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
|
||||
return DaemonKindExitImmediately
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewExitImmediatelyInstruction() Instruction {
|
||||
return &ExitImmediatelyInstruction{}
|
||||
}
|
||||
|
||||
type 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,
|
||||
}
|
||||
} else if strings.HasSuffix(os.Args[1], 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")
|
||||
})
|
||||
|
||||
todo := TodoLinesToString(todoLines)
|
||||
|
||||
return &CherryPickCommitsInstruction{
|
||||
Todo: todo,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
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,
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
type exitImmediatelyDaemon struct {
|
||||
c *common.Common
|
||||
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindChangeTodoActions
|
||||
}
|
||||
|
||||
func (self *exitImmediatelyDaemon) Run() error {
|
||||
return nil
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveTodoUp
|
||||
}
|
||||
|
||||
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"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+28
-27
@@ -87,33 +87,34 @@ func writeString(file *os.File, str string) {
|
||||
|
||||
func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
contextTitleMap := map[string]string{
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commitDescription": tr.CommitDescriptionTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
}
|
||||
|
||||
title, ok := contextTitleMap[str]
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
var ErrInvalidCommitIndex = errors.New("invalid commit index")
|
||||
|
||||
type CommitCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -18,11 +20,6 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
|
||||
}
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
// ResetAuthor resets the author of the topmost commit
|
||||
func (self *CommitCommands) ResetAuthor() error {
|
||||
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
|
||||
@@ -45,11 +42,7 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
|
||||
}
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
|
||||
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
|
||||
noVerifyFlag := ""
|
||||
@@ -57,7 +50,23 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
noVerifyFlag = " --no-verify"
|
||||
}
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs))
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) commitMessageArgs(message string) string {
|
||||
msg, description, _ := strings.Cut(message, "\n")
|
||||
descriptionArgs := ""
|
||||
if description != "" {
|
||||
descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs)
|
||||
}
|
||||
|
||||
// runs git commit without the -m argument meaning it will invoke the user's editor
|
||||
@@ -178,3 +187,13 @@ func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
|
||||
func (self *CommitCommands) CreateFixupCommit(sha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
|
||||
}
|
||||
|
||||
// a value of 0 means the head commit, 1 is the parent commit, etc
|
||||
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
|
||||
hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput()
|
||||
formattedHash := strings.TrimSpace(hash)
|
||||
if len(formattedHash) == 0 {
|
||||
return "", ErrInvalidCommitIndex
|
||||
}
|
||||
return self.GetCommitMessage(formattedHash)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
@@ -194,8 +194,8 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := slices.Map(commits, func(commit *models.Commit) string {
|
||||
return commit.Sha
|
||||
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
|
||||
return commit.Sha, commit.Sha != ""
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
@@ -209,20 +209,26 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
|
||||
),
|
||||
).DontLog()
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
fullCommits := map[string]*models.Commit{}
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
fullCommits[commit.Sha] = commit
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
for _, rebasingCommit := range commits {
|
||||
if rebasingCommit.Sha == "" {
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := fullCommits[rebasingCommit.Sha]; commit != nil {
|
||||
commit.Action = rebasingCommit.Action
|
||||
commit.Status = rebasingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
}
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
@@ -305,15 +311,17 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Commit == "" {
|
||||
if t.Command == todo.UpdateRef {
|
||||
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
|
||||
} else if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = slices.Prepend(commits, &models.Commit{
|
||||
Sha: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: "rebasing",
|
||||
Action: t.Command.String(),
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -332,7 +340,7 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
Status: models.StatusRebasing,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,11 +357,11 @@ func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*mod
|
||||
if strings.HasPrefix(ancestor, commit.Sha) {
|
||||
passedAncestor = true
|
||||
}
|
||||
if commit.Status != "pushed" {
|
||||
if commit.Status != models.StatusPushed {
|
||||
continue
|
||||
}
|
||||
if passedAncestor {
|
||||
commits[i].Status = "merged"
|
||||
commits[i].Status = models.StatusMerged
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
|
||||
@@ -78,8 +78,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: "unpushed",
|
||||
Action: "",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -92,8 +92,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
|
||||
Name: "fix logging",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -106,8 +106,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
|
||||
Name: "refactor",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -120,8 +120,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -134,8 +134,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -148,8 +148,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -162,8 +162,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
@@ -176,8 +176,8 @@ func TestGetCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
|
||||
Name: "refactoring the config struct",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
|
||||
@@ -9,12 +9,32 @@ import (
|
||||
)
|
||||
|
||||
func TestCommitRewordCommit(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{runner: runner})
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
input string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Single line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
|
||||
"test",
|
||||
},
|
||||
{
|
||||
"Multi line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
|
||||
"test\nline 2\nline 3",
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
assert.NoError(t, instance.RewordLastCommit("test"))
|
||||
runner.CheckForMissingCalls()
|
||||
assert.NoError(t, instance.RewordLastCommit(s.input))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitResetToCommit(t *testing.T) {
|
||||
@@ -274,3 +294,40 @@ Merge pull request #1750 from mark2185/fix-issue-template
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitMessageFromHistory(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(string, error)
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Empty message",
|
||||
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "", nil).Expect("git rev-list --format=%B --max-count=1 ", "", nil),
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Default case to retrieve a commit in history",
|
||||
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "sha3 \n", nil).Expect("git rev-list --format=%B --max-count=1 sha3", `commit sha3
|
||||
use generics to DRY up context code`, nil),
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "use generics to DRY up context code", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
output, err := instance.GetCommitMessageFromHistory(2)
|
||||
|
||||
s.test(output, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +105,17 @@ 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(commits[baseIndex].Sha, todoLines, true).Run()
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: commits[baseIndex].Sha,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+124
-145
@@ -2,15 +2,17 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"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 {
|
||||
@@ -54,12 +56,16 @@ 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(sha, todo, false), nil
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
|
||||
@@ -75,7 +81,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
|
||||
if index == 0 {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return f()
|
||||
}
|
||||
@@ -95,41 +101,79 @@ 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)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(baseShaOrRoot, todoLines, true).Run()
|
||||
sha := commits[index].Sha
|
||||
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
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)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
sha := commits[index].Sha
|
||||
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
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})
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
|
||||
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[index].Sha,
|
||||
NewAction: action,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) EditRebase(branchRef string) error {
|
||||
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: branchRef,
|
||||
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
|
||||
instruction daemon.Instruction
|
||||
overrideEditor 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(baseShaOrRoot string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
|
||||
todo := self.buildTodo(todoLines)
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
@@ -137,119 +181,75 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseShaOrRoot string
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash %s", 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,
|
||||
"DEBUG="+debug,
|
||||
"LANG=en_US.UTF-8", // Force using EN as language
|
||||
"LC_ALL=en_US.UTF-8", // Force using EN as language
|
||||
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
if opts.overrideEditor {
|
||||
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
|
||||
}
|
||||
|
||||
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 at a given index in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := os.ReadFile(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
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.getTodoCommitCount(content)
|
||||
|
||||
// we have the most recent commit at the bottom whereas the todo file has
|
||||
// it at the bottom, so we need to subtract our index from the commit count
|
||||
contentIndex := commitCount - 1 - index
|
||||
splitLine := strings.Split(content[contentIndex], " ")
|
||||
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
|
||||
result := strings.Join(content, "\n")
|
||||
|
||||
return os.WriteFile(fileName, []byte(result), 0o644)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) getTodoCommitCount(content []string) int {
|
||||
// count lines that are not blank and are not comments
|
||||
commitCount := 0
|
||||
for _, line := range content {
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
commitCount++
|
||||
}
|
||||
}
|
||||
return commitCount
|
||||
// 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
|
||||
func (self *RebaseCommands) MoveTodoDown(index int) error {
|
||||
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.getTodoCommitCount(content)
|
||||
contentIndex := commitCount - 1 - index
|
||||
|
||||
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
|
||||
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
|
||||
result := strings.Join(rearrangedContent, "\n")
|
||||
|
||||
return os.WriteFile(fileName, []byte(result), 0o644)
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
@@ -283,17 +283,22 @@ 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(sha, todo, true).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (self *RebaseCommands) RebaseBranch(branchName string) error {
|
||||
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
@@ -334,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()
|
||||
}
|
||||
|
||||
@@ -376,42 +382,15 @@ 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")
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
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 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"
|
||||
}
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: "HEAD",
|
||||
instruction: daemon.NewCherryPickCommitsInstruction(commits),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
// we can't start an interactive rebase from the first commit without passing the
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -52,7 +52,7 @@ func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
Parents: parents,
|
||||
}
|
||||
|
||||
|
||||
@@ -51,35 +51,35 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from master to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "f4ddf2f0d4be4ccc7efa",
|
||||
Name: "checkout: moving from A to master",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643149435,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -95,7 +95,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -119,7 +119,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
@@ -128,7 +128,7 @@ func TestGetReflogCommits(t *testing.T) {
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
|
||||
@@ -29,11 +29,7 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
|
||||
cmdStr := "git push"
|
||||
|
||||
if opts.Force {
|
||||
if self.version.IsOlderThan(2, 30, 0) {
|
||||
cmdStr += " --force-with-lease"
|
||||
} else {
|
||||
cmdStr += " --force-with-lease --force-if-includes"
|
||||
}
|
||||
cmdStr += " --force-with-lease"
|
||||
}
|
||||
|
||||
if opts.SetUpstream {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
func TestSyncPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
version *GitVersion
|
||||
opts PushOpts
|
||||
test func(oscommands.ICmdObj, error)
|
||||
}
|
||||
@@ -18,7 +17,6 @@ func TestSyncPush(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Push with force disabled",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{Force: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push")
|
||||
@@ -27,25 +25,14 @@ func TestSyncPush(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled (>= 2.30.0)",
|
||||
version: &GitVersion{2, 30, 0, ""},
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease --force-if-includes")
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
@@ -58,7 +45,6 @@ func TestSyncPush(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
@@ -72,7 +58,6 @@ func TestSyncPush(t *testing.T) {
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
@@ -84,23 +69,8 @@ func TestSyncPush(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream (>= 2.30.0)",
|
||||
version: &GitVersion{2, 30, 0, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --force-if-includes --set-upstream "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with remote branch but no origin",
|
||||
version: &GitVersion{2, 29, 3, ""},
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
@@ -117,7 +87,7 @@ func TestSyncPush(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildSyncCommands(commonDeps{gitVersion: s.version})
|
||||
instance := buildSyncCommands(commonDeps{})
|
||||
s.test(instance.PushCmdObj(s.opts))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@ func (self *TagCommands) Delete(tagName string) error {
|
||||
}
|
||||
|
||||
func (self *TagCommands) Push(remoteName string, tagName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
return self.cmd.New(fmt.Sprintf("git push %s tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func ParseGitVersion(versionStr string) (*GitVersion, error) {
|
||||
// versionStr should be something like:
|
||||
// git version 2.39.0
|
||||
// git version 2.37.1 (Apple Git-137.1)
|
||||
re := regexp.MustCompile(`[^\d]+(\d+)(\.\d+)?(\.\d+)?(.*)`)
|
||||
re := regexp.MustCompile(`[^\d]*(\d+)(\.\d+)?(\.\d+)?(.*)`)
|
||||
matches := re.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 5 {
|
||||
@@ -65,3 +65,7 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
|
||||
required := major*1000*1000 + minor*1000 + patch
|
||||
return actual < required
|
||||
}
|
||||
|
||||
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
|
||||
@@ -64,12 +64,22 @@ var bitbucketServerServiceDef = ServiceDefinition{
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
}
|
||||
|
||||
var giteaServiceDef = ServiceDefinition{
|
||||
provider: "gitea",
|
||||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{
|
||||
githubServiceDef,
|
||||
bitbucketServiceDef,
|
||||
gitLabServiceDef,
|
||||
azdoServiceDef,
|
||||
bitbucketServerServiceDef,
|
||||
giteaServiceDef,
|
||||
}
|
||||
|
||||
var defaultServiceDomains = []ServiceDomain{
|
||||
@@ -93,4 +103,9 @@ var defaultServiceDomains = []ServiceDomain{
|
||||
gitDomain: "dev.azure.com",
|
||||
webDomain: "dev.azure.com",
|
||||
},
|
||||
{
|
||||
serviceDefinition: giteaServiceDef,
|
||||
gitDomain: "try.gitea.io",
|
||||
webDomain: "try.gitea.io",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
)
|
||||
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
|
||||
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
|
||||
// and this package's responsibility is to determine which service you're using based on the remote URL,
|
||||
// and then which URL you need for whatever use case you have.
|
||||
|
||||
@@ -264,6 +264,60 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
@@ -310,7 +364,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer, gitea"},
|
||||
},
|
||||
{
|
||||
testName: "Escapes reserved URL characters in from branch name",
|
||||
|
||||
@@ -3,18 +3,37 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Special commit hash for empty tree object
|
||||
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
type CommitStatus int
|
||||
|
||||
const (
|
||||
StatusNone CommitStatus = iota
|
||||
StatusUnpushed
|
||||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
const (
|
||||
// Conveniently for us, the todo package starts the enum at 1, and given
|
||||
// that it doesn't have a "none" value, we're setting ours to 0
|
||||
ActionNone todo.TodoCommand = 0
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
@@ -63,7 +82,7 @@ func (c *Commit) IsMerge() bool {
|
||||
// returns true if this commit is not actually in the git log but instead
|
||||
// is from a TODO file for an interactive rebase.
|
||||
func (c *Commit) IsTODO() bool {
|
||||
return c.Action != ""
|
||||
return c.Action != ActionNone
|
||||
}
|
||||
|
||||
func IsHeadCommit(commits []*Commit, index int) bool {
|
||||
|
||||
@@ -37,11 +37,12 @@ type editPreset struct {
|
||||
|
||||
func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset {
|
||||
presets := map[string]*editPreset{
|
||||
"vi": standardTerminalEditorPreset("vi"),
|
||||
"vim": standardTerminalEditorPreset("vim"),
|
||||
"nvim": standardTerminalEditorPreset("nvim"),
|
||||
"emacs": standardTerminalEditorPreset("emacs"),
|
||||
"nano": standardTerminalEditorPreset("nano"),
|
||||
"vi": standardTerminalEditorPreset("vi"),
|
||||
"vim": standardTerminalEditorPreset("vim"),
|
||||
"nvim": standardTerminalEditorPreset("nvim"),
|
||||
"emacs": standardTerminalEditorPreset("emacs"),
|
||||
"nano": standardTerminalEditorPreset("nano"),
|
||||
"kakoune": standardTerminalEditorPreset("kakoune"),
|
||||
"vscode": {
|
||||
editTemplate: "code --reuse-window -- {{filename}}",
|
||||
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
|
||||
@@ -70,6 +71,7 @@ func getPreset(osConfig *OSConfig, guessDefaultEditor func() string) *editPreset
|
||||
|
||||
// Some of our presets have a different name than the editor they are using.
|
||||
editorToPreset := map[string]string{
|
||||
"kak": "kakoune",
|
||||
"code": "vscode",
|
||||
"subl": "sublime",
|
||||
"xed": "xcode",
|
||||
|
||||
@@ -165,6 +165,7 @@ type KeybindingUniversalConfig struct {
|
||||
Select string `yaml:"select"`
|
||||
GoInto string `yaml:"goInto"`
|
||||
Confirm string `yaml:"confirm"`
|
||||
ConfirmInEditor string `yaml:"confirmInEditor"`
|
||||
Remove string `yaml:"remove"`
|
||||
New string `yaml:"new"`
|
||||
Edit string `yaml:"edit"`
|
||||
@@ -193,7 +194,6 @@ type KeybindingUniversalConfig struct {
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
|
||||
@@ -492,6 +492,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Select: "<space>",
|
||||
GoInto: "<enter>",
|
||||
Confirm: "<enter>",
|
||||
ConfirmInEditor: "<a-enter>",
|
||||
Remove: "d",
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
@@ -520,7 +521,6 @@ func GetDefaultConfig() *UserConfig {
|
||||
DiffingMenuAlt: "<c-e>",
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
ToggleWhitespaceInDiffView: "<c-w>",
|
||||
IncreaseContextInDiffView: "}",
|
||||
|
||||
@@ -15,7 +15,6 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
map[string]string{
|
||||
"keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return),
|
||||
"keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm),
|
||||
"keyBindNewLine": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.AppendNewline),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -34,6 +35,13 @@ func (gui *Gui) branchCommitsRenderToMain() error {
|
||||
commit := gui.State.Contexts.LocalCommits.GetSelected()
|
||||
if commit == nil {
|
||||
task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch)
|
||||
} else if commit.Action == todo.UpdateRef {
|
||||
task = types.NewRenderStringTask(
|
||||
utils.ResolvePlaceholderString(
|
||||
gui.c.Tr.UpdateRefHere,
|
||||
map[string]string{
|
||||
"ref": commit.Name,
|
||||
}))
|
||||
} else {
|
||||
cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(),
|
||||
gui.IgnoreWhitespaceInDiffView)
|
||||
|
||||
@@ -78,17 +78,11 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||
return lineCount
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, int, int, int) {
|
||||
panelWidth := gui.getConfirmationPanelWidth()
|
||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
||||
return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
|
||||
func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
|
||||
return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight)
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
|
||||
return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight)
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
|
||||
func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
|
||||
width, height := gui.g.Size()
|
||||
if panelHeight > height*3/4 {
|
||||
panelHeight = height * 3 / 4
|
||||
@@ -186,7 +180,7 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane
|
||||
}
|
||||
confirmationView := gui.Views.Confirmation
|
||||
confirmationView.Editable = opts.Editable
|
||||
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
confirmationView.Editor = gocui.EditorFunc(gui.promptEditor)
|
||||
|
||||
if opts.Editable {
|
||||
textArea := confirmationView.TextArea
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// This file is for the management of contexts. There is a context stack such that
|
||||
@@ -146,6 +147,36 @@ func (gui *Gui) popContext() error {
|
||||
return gui.activateContext(newContext, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
func (gui *Gui) removeContexts(contextsToRemove []types.Context) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool {
|
||||
for _, contextToRemove := range contextsToRemove {
|
||||
if context.GetKey() == contextToRemove.GetKey() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
gui.State.ContextManager.ContextStack = rest
|
||||
contextToActivate := rest[len(rest)-1]
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
for _, context := range contextsToRemove {
|
||||
if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// activate the item at the top of the stack
|
||||
return gui.activateContext(contextToActivate, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
|
||||
view, _ := gui.g.View(c.GetViewName())
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type CommitMessageContext struct {
|
||||
types.Context
|
||||
viewModel *CommitMessageViewModel
|
||||
}
|
||||
|
||||
// when selectedIndex (see below) is set to this value, it means that we're not
|
||||
// currently viewing a commit message of an existing commit: instead we're making our own
|
||||
// new commit message
|
||||
const NoCommitIndex = -1
|
||||
|
||||
type CommitMessageViewModel struct {
|
||||
// index of the commit message, where -1 is 'no commit', 0 is the HEAD commit, 1
|
||||
// is the prior commit, and so on
|
||||
selectedindex int
|
||||
// if true, then upon escaping from the commit message panel, we will preserve
|
||||
// the message so that it's still shown next time we open the panel
|
||||
preserveMessage bool
|
||||
// the full preserved message (combined summary and description)
|
||||
preservedMessage string
|
||||
// invoked when pressing enter in the commit message panel
|
||||
onConfirm func(string) error
|
||||
|
||||
// The message typed in before cycling through history
|
||||
// We store this separately to 'preservedMessage' because 'preservedMessage'
|
||||
// is specifically for committing staged files and we don't want this affected
|
||||
// by cycling through history in the context of rewording an old commit.
|
||||
historyMessage string
|
||||
}
|
||||
|
||||
func NewCommitMessageContext(
|
||||
view *gocui.View,
|
||||
opts ContextCallbackOpts,
|
||||
) *CommitMessageContext {
|
||||
viewModel := &CommitMessageViewModel{}
|
||||
return &CommitMessageContext{
|
||||
viewModel: viewModel,
|
||||
Context: NewSimpleContext(
|
||||
NewBaseContext(NewBaseContextOpts{
|
||||
Kind: types.PERSISTENT_POPUP,
|
||||
View: view,
|
||||
WindowName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
Focusable: true,
|
||||
HasUncontrolledBounds: true,
|
||||
}),
|
||||
opts,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) SetSelectedIndex(value int) {
|
||||
self.viewModel.selectedindex = value
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) GetSelectedIndex() int {
|
||||
return self.viewModel.selectedindex
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) GetPreserveMessage() bool {
|
||||
return self.viewModel.preserveMessage
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) GetPreservedMessage() string {
|
||||
return self.viewModel.preservedMessage
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) SetPreservedMessage(message string) {
|
||||
self.viewModel.preservedMessage = message
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) GetHistoryMessage() string {
|
||||
return self.viewModel.historyMessage
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) SetHistoryMessage(message string) {
|
||||
self.viewModel.historyMessage = message
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) OnConfirm(message string) error {
|
||||
return self.viewModel.onConfirm(message)
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) {
|
||||
self.viewModel.selectedindex = index
|
||||
self.viewModel.preserveMessage = preserveMessage
|
||||
self.viewModel.onConfirm = onConfirm
|
||||
self.GetView().Title = title
|
||||
}
|
||||
@@ -33,13 +33,14 @@ const (
|
||||
INFORMATION_CONTEXT_KEY types.ContextKey = "information"
|
||||
LIMIT_CONTEXT_KEY types.ContextKey = "limit"
|
||||
|
||||
MENU_CONTEXT_KEY types.ContextKey = "menu"
|
||||
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY types.ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
|
||||
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
|
||||
MENU_CONTEXT_KEY types.ContextKey = "menu"
|
||||
CONFIRMATION_CONTEXT_KEY types.ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY types.ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY types.ContextKey = "commitMessage"
|
||||
COMMIT_DESCRIPTION_CONTEXT_KEY types.ContextKey = "commitDescription"
|
||||
SUBMODULES_CONTEXT_KEY types.ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY types.ContextKey = "suggestions"
|
||||
COMMAND_LOG_CONTEXT_KEY types.ContextKey = "cmdLog"
|
||||
)
|
||||
|
||||
var AllContextKeys = []types.ContextKey{
|
||||
@@ -97,7 +98,8 @@ type ContextTree struct {
|
||||
CustomPatchBuilderSecondary types.Context
|
||||
MergeConflicts *MergeConflictsContext
|
||||
Confirmation types.Context
|
||||
CommitMessage types.Context
|
||||
CommitMessage *CommitMessageContext
|
||||
CommitDescription types.Context
|
||||
CommandLog types.Context
|
||||
|
||||
// display contexts
|
||||
@@ -129,6 +131,7 @@ func (self *ContextTree) Flatten() []types.Context {
|
||||
self.Menu,
|
||||
self.Confirmation,
|
||||
self.CommitMessage,
|
||||
self.CommitDescription,
|
||||
|
||||
self.MergeConflicts,
|
||||
self.StagingSecondary,
|
||||
|
||||
@@ -217,17 +217,29 @@ func (gui *Gui) contextTree() *context.ContextTree {
|
||||
},
|
||||
},
|
||||
),
|
||||
CommitMessage: context.NewSimpleContext(
|
||||
CommitMessage: context.NewCommitMessageContext(
|
||||
gui.Views.CommitMessage,
|
||||
context.ContextCallbackOpts{
|
||||
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
|
||||
},
|
||||
),
|
||||
CommitDescription: context.NewSimpleContext(
|
||||
context.NewBaseContext(context.NewBaseContextOpts{
|
||||
Kind: types.PERSISTENT_POPUP,
|
||||
View: gui.Views.CommitMessage,
|
||||
WindowName: "commitMessage",
|
||||
Key: context.COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
View: gui.Views.CommitDescription,
|
||||
WindowName: "commitDescription",
|
||||
Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY,
|
||||
Focusable: true,
|
||||
HasUncontrolledBounds: true,
|
||||
}),
|
||||
context.ContextCallbackOpts{
|
||||
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
|
||||
OnFocus: func(opts types.OnFocusOpts) error {
|
||||
_, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
),
|
||||
Search: context.NewSimpleContext(
|
||||
|
||||
+28
-26
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/snake"
|
||||
)
|
||||
|
||||
@@ -26,10 +25,24 @@ func (gui *Gui) resetControllers() {
|
||||
|
||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
|
||||
setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||
getSavedCommitMessage := func() string {
|
||||
return gui.State.savedCommitMessage
|
||||
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
||||
getCommitSummary := func() string {
|
||||
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
|
||||
}
|
||||
|
||||
getCommitDescription := func() string {
|
||||
return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent())
|
||||
}
|
||||
commitsHelper := helpers.NewCommitsHelper(helperCommon,
|
||||
gui.State.Model,
|
||||
gui.State.Contexts,
|
||||
getCommitSummary,
|
||||
setCommitSummary,
|
||||
getCommitDescription,
|
||||
setCommitDescription,
|
||||
gui.RenderCommitLength,
|
||||
)
|
||||
gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git)
|
||||
gui.helpers = &helpers.Helpers{
|
||||
Refs: refsHelper,
|
||||
@@ -38,7 +51,7 @@ func (gui *Gui) resetControllers() {
|
||||
Bisect: helpers.NewBisectHelper(helperCommon, gui.git),
|
||||
Suggestions: suggestionsHelper,
|
||||
Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand),
|
||||
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitMessage, getSavedCommitMessage),
|
||||
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper),
|
||||
Tags: helpers.NewTagsHelper(helperCommon, gui.git),
|
||||
GPG: gpgHelper,
|
||||
MergeAndRebase: rebaseHelper,
|
||||
@@ -52,6 +65,7 @@ func (gui *Gui) resetControllers() {
|
||||
),
|
||||
Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc),
|
||||
AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper),
|
||||
Commits: commitsHelper,
|
||||
}
|
||||
|
||||
gui.CustomCommandsClient = custom_commands.NewClient(
|
||||
@@ -84,27 +98,12 @@ func (gui *Gui) resetControllers() {
|
||||
|
||||
bisectController := controllers.NewBisectController(common)
|
||||
|
||||
getCommitMessage := func() string {
|
||||
return strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
|
||||
}
|
||||
|
||||
onCommitAttempt := func(message string) {
|
||||
gui.State.savedCommitMessage = message
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
}
|
||||
|
||||
onCommitSuccess := func() {
|
||||
gui.State.savedCommitMessage = ""
|
||||
_ = gui.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.STAGING},
|
||||
})
|
||||
}
|
||||
|
||||
commitMessageController := controllers.NewCommitMessageController(
|
||||
common,
|
||||
getCommitMessage,
|
||||
onCommitAttempt,
|
||||
onCommitSuccess,
|
||||
)
|
||||
|
||||
commitDescriptionController := controllers.NewCommitDescriptionController(
|
||||
common,
|
||||
)
|
||||
|
||||
remoteBranchesController := controllers.NewRemoteBranchesController(common)
|
||||
@@ -115,8 +114,7 @@ func (gui *Gui) resetControllers() {
|
||||
filesController := controllers.NewFilesController(
|
||||
common,
|
||||
gui.enterSubmodule,
|
||||
setCommitMessage,
|
||||
getSavedCommitMessage,
|
||||
setCommitSummary,
|
||||
)
|
||||
mergeConflictsController := controllers.NewMergeConflictsController(common)
|
||||
remotesController := controllers.NewRemotesController(
|
||||
@@ -250,6 +248,10 @@ func (gui *Gui) resetControllers() {
|
||||
commitMessageController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.CommitDescription,
|
||||
commitDescriptionController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.RemoteBranches,
|
||||
remoteBranchesController,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type CommitDescriptionController struct {
|
||||
baseController
|
||||
*controllerCommon
|
||||
}
|
||||
|
||||
var _ types.IController = &CommitMessageController{}
|
||||
|
||||
func NewCommitDescriptionController(
|
||||
common *controllerCommon,
|
||||
) *CommitDescriptionController {
|
||||
return &CommitDescriptionController{
|
||||
baseController: baseController{},
|
||||
controllerCommon: common,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.switchToCommitMessage,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Return),
|
||||
Handler: self.close,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor),
|
||||
Handler: self.confirm,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) Context() types.Context {
|
||||
return self.context()
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) context() types.Context {
|
||||
return self.contexts.CommitMessage
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) switchToCommitMessage() error {
|
||||
return self.c.PushContext(self.contexts.CommitMessage)
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) close() error {
|
||||
return self.helpers.Commits.CloseCommitMessagePanel()
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionController) confirm() error {
|
||||
return self.helpers.Commits.HandleCommitConfirm()
|
||||
}
|
||||
@@ -1,33 +1,24 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type CommitMessageController struct {
|
||||
baseController
|
||||
*controllerCommon
|
||||
|
||||
getCommitMessage func() string
|
||||
onCommitAttempt func(message string)
|
||||
onCommitSuccess func()
|
||||
}
|
||||
|
||||
var _ types.IController = &CommitMessageController{}
|
||||
|
||||
func NewCommitMessageController(
|
||||
common *controllerCommon,
|
||||
getCommitMessage func() string,
|
||||
onCommitAttempt func(message string),
|
||||
onCommitSuccess func(),
|
||||
) *CommitMessageController {
|
||||
return &CommitMessageController{
|
||||
baseController: baseController{},
|
||||
controllerCommon: common,
|
||||
|
||||
getCommitMessage: getCommitMessage,
|
||||
onCommitAttempt: onCommitAttempt,
|
||||
onCommitSuccess: onCommitSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +32,18 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts)
|
||||
Key: opts.GetKey(opts.Config.Universal.Return),
|
||||
Handler: self.close,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.PrevItem),
|
||||
Handler: self.handlePreviousCommit,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.NextItem),
|
||||
Handler: self.handleNextCommit,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.switchToCommitDescription,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
@@ -50,30 +53,63 @@ func (self *CommitMessageController) Context() types.Context {
|
||||
return self.context()
|
||||
}
|
||||
|
||||
// this method is pointless in this context but I'm keeping it consistent
|
||||
// with other contexts so that when generics arrive it's easier to refactor
|
||||
func (self *CommitMessageController) context() types.Context {
|
||||
func (self *CommitMessageController) context() *context.CommitMessageContext {
|
||||
return self.contexts.CommitMessage
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) confirm() error {
|
||||
message := self.getCommitMessage()
|
||||
self.onCommitAttempt(message)
|
||||
func (self *CommitMessageController) handlePreviousCommit() error {
|
||||
return self.handleCommitIndexChange(1)
|
||||
}
|
||||
|
||||
if message == "" {
|
||||
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
|
||||
func (self *CommitMessageController) handleNextCommit() error {
|
||||
if self.context().GetSelectedIndex() == context.NoCommitIndex {
|
||||
return nil
|
||||
}
|
||||
return self.handleCommitIndexChange(-1)
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) switchToCommitDescription() error {
|
||||
if err := self.c.PushContext(self.contexts.CommitDescription); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) handleCommitIndexChange(value int) error {
|
||||
currentIndex := self.context().GetSelectedIndex()
|
||||
newIndex := currentIndex + value
|
||||
if newIndex == context.NoCommitIndex {
|
||||
self.context().SetSelectedIndex(newIndex)
|
||||
self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage())
|
||||
return nil
|
||||
} else if currentIndex == context.NoCommitIndex {
|
||||
self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription())
|
||||
}
|
||||
|
||||
cmdObj := self.git.Commit.CommitCmdObj(message)
|
||||
self.c.LogAction(self.c.Tr.Actions.Commit)
|
||||
validCommit, err := self.setCommitMessageAtIndex(newIndex)
|
||||
if validCommit {
|
||||
self.context().SetSelectedIndex(newIndex)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_ = self.c.PopContext()
|
||||
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
|
||||
self.onCommitSuccess()
|
||||
return nil
|
||||
})
|
||||
// returns true if the given index is for a valid commit
|
||||
func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) {
|
||||
commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index)
|
||||
if err != nil {
|
||||
if err == git_commands.ErrInvalidCommitIndex {
|
||||
return false, nil
|
||||
}
|
||||
return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
self.helpers.Commits.UpdateCommitPanelView(commitMessage)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) confirm() error {
|
||||
return self.helpers.Commits.HandleCommitConfirm()
|
||||
}
|
||||
|
||||
func (self *CommitMessageController) close() error {
|
||||
return self.c.PopContext()
|
||||
return self.helpers.Commits.CloseCommitMessagePanel()
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ type FilesController struct {
|
||||
baseController // nolint: unused
|
||||
*controllerCommon
|
||||
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error
|
||||
setCommitMessage func(message string)
|
||||
getSavedCommitMessage func() string
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error
|
||||
setCommitMessage func(message string)
|
||||
}
|
||||
|
||||
var _ types.IController = &FilesController{}
|
||||
@@ -26,13 +25,11 @@ func NewFilesController(
|
||||
common *controllerCommon,
|
||||
enterSubmodule func(submodule *models.SubmoduleConfig) error,
|
||||
setCommitMessage func(message string),
|
||||
getSavedCommitMessage func() string,
|
||||
) *FilesController {
|
||||
return &FilesController{
|
||||
controllerCommon: common,
|
||||
enterSubmodule: enterSubmodule,
|
||||
setCommitMessage: setCommitMessage,
|
||||
getSavedCommitMessage: getSavedCommitMessage,
|
||||
controllerCommon: common,
|
||||
enterSubmodule: enterSubmodule,
|
||||
setCommitMessage: setCommitMessage,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type ICommitsHelper interface {
|
||||
UpdateCommitPanelView(message string)
|
||||
}
|
||||
|
||||
type CommitsHelper struct {
|
||||
c *types.HelperCommon
|
||||
|
||||
model *types.Model
|
||||
contexts *context.ContextTree
|
||||
getCommitSummary func() string
|
||||
setCommitSummary func(string)
|
||||
getCommitDescription func() string
|
||||
setCommitDescription func(string)
|
||||
renderCommitLength func()
|
||||
}
|
||||
|
||||
var _ ICommitsHelper = &CommitsHelper{}
|
||||
|
||||
func NewCommitsHelper(
|
||||
c *types.HelperCommon,
|
||||
model *types.Model,
|
||||
contexts *context.ContextTree,
|
||||
getCommitSummary func() string,
|
||||
setCommitSummary func(string),
|
||||
getCommitDescription func() string,
|
||||
setCommitDescription func(string),
|
||||
renderCommitLength func(),
|
||||
) *CommitsHelper {
|
||||
return &CommitsHelper{
|
||||
c: c,
|
||||
model: model,
|
||||
contexts: contexts,
|
||||
getCommitSummary: getCommitSummary,
|
||||
setCommitSummary: setCommitSummary,
|
||||
getCommitDescription: getCommitDescription,
|
||||
setCommitDescription: setCommitDescription,
|
||||
renderCommitLength: renderCommitLength,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) SplitCommitMessageAndDescription(message string) (string, string) {
|
||||
for _, separator := range []string{"\n\n", "\n\r\n\r", "\n", "\n\r"} {
|
||||
msg, description, found := strings.Cut(message, separator)
|
||||
if found {
|
||||
return msg, description
|
||||
}
|
||||
}
|
||||
return message, ""
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) {
|
||||
summary, description := self.SplitCommitMessageAndDescription(message)
|
||||
|
||||
self.setCommitSummary(summary)
|
||||
self.setCommitDescription(description)
|
||||
self.renderCommitLength()
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
|
||||
if len(self.getCommitDescription()) == 0 {
|
||||
return self.getCommitSummary()
|
||||
}
|
||||
return self.getCommitSummary() + "\n" + self.getCommitDescription()
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) UpdateCommitPanelView(message string) {
|
||||
// first try the passed in message, if not fallback to context -> view in that order
|
||||
if message != "" {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
return
|
||||
}
|
||||
message = self.contexts.CommitMessage.GetPreservedMessage()
|
||||
if message != "" {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
} else {
|
||||
self.SetMessageAndDescriptionInView(self.getCommitSummary())
|
||||
}
|
||||
}
|
||||
|
||||
type OpenCommitMessagePanelOpts struct {
|
||||
CommitIndex int
|
||||
Title string
|
||||
PreserveMessage bool
|
||||
OnConfirm func(string) error
|
||||
InitialMessage string
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error {
|
||||
self.contexts.CommitMessage.SetPanelState(
|
||||
opts.CommitIndex,
|
||||
opts.Title,
|
||||
opts.PreserveMessage,
|
||||
opts.OnConfirm,
|
||||
)
|
||||
|
||||
self.UpdateCommitPanelView(opts.InitialMessage)
|
||||
|
||||
return self.pushCommitMessageContexts()
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) OnCommitSuccess() {
|
||||
// if we have a preserved message we want to clear it on success
|
||||
if self.contexts.CommitMessage.GetPreserveMessage() {
|
||||
self.contexts.CommitMessage.SetPreservedMessage("")
|
||||
}
|
||||
self.SetMessageAndDescriptionInView("")
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) HandleCommitConfirm() error {
|
||||
fullMessage := self.JoinCommitMessageAndDescription()
|
||||
|
||||
if fullMessage == "" {
|
||||
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
|
||||
err := self.contexts.CommitMessage.OnConfirm(fullMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) CloseCommitMessagePanel() error {
|
||||
if self.contexts.CommitMessage.GetPreserveMessage() {
|
||||
message := self.JoinCommitMessageAndDescription()
|
||||
|
||||
self.contexts.CommitMessage.SetPreservedMessage(message)
|
||||
} else {
|
||||
self.SetMessageAndDescriptionInView("")
|
||||
}
|
||||
|
||||
self.contexts.CommitMessage.SetHistoryMessage("")
|
||||
|
||||
return self.PopCommitMessageContexts()
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) PopCommitMessageContexts() error {
|
||||
return self.c.RemoveContexts(self.commitMessageContexts())
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) pushCommitMessageContexts() error {
|
||||
for _, context := range self.commitMessageContexts() {
|
||||
if err := self.c.PushContext(context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) commitMessageContexts() []types.Context {
|
||||
return []types.Context{
|
||||
self.contexts.CommitDescription,
|
||||
self.contexts.CommitMessage,
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Helpers struct {
|
||||
GPG *GpgHelper
|
||||
Upstream *UpstreamHelper
|
||||
AmendHelper *AmendHelper
|
||||
Commits *CommitsHelper
|
||||
}
|
||||
|
||||
func NewStubHelpers() *Helpers {
|
||||
@@ -33,5 +34,6 @@ func NewStubHelpers() *Helpers {
|
||||
GPG: &GpgHelper{},
|
||||
Upstream: &UpstreamHelper{},
|
||||
AmendHelper: &AmendHelper{},
|
||||
Commits: &CommitsHelper{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,22 +201,42 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
|
||||
if ref == checkedOutBranch {
|
||||
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
|
||||
}
|
||||
prompt := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.ConfirmRebase,
|
||||
menuItems := []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.SimpleRebase,
|
||||
Key: 's',
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
err := self.git.Rebase.RebaseBranch(ref)
|
||||
return self.CheckMergeOrRebase(err)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.InteractiveRebase,
|
||||
Key: 'i',
|
||||
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
|
||||
OnPress: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
err := self.git.Rebase.EditRebase(ref)
|
||||
if err = self.CheckMergeOrRebase(err); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.c.PushContext(self.contexts.LocalCommits)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
title := utils.ResolvePlaceholderString(
|
||||
self.c.Tr.RebasingTitle,
|
||||
map[string]string{
|
||||
"checkedOutBranch": checkedOutBranch,
|
||||
"selectedBranch": ref,
|
||||
"ref": ref,
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.RebasingTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
|
||||
err := self.git.Rebase.RebaseBranch(ref)
|
||||
return self.CheckMergeOrRebase(err)
|
||||
},
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: title,
|
||||
Items: menuItems,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ type IWorkingTreeHelper interface {
|
||||
}
|
||||
|
||||
type WorkingTreeHelper struct {
|
||||
c *types.HelperCommon
|
||||
git *commands.GitCommand
|
||||
contexts *context.ContextTree
|
||||
refHelper *RefsHelper
|
||||
model *types.Model
|
||||
setCommitMessage func(message string)
|
||||
getSavedCommitMessage func() string
|
||||
c *types.HelperCommon
|
||||
git *commands.GitCommand
|
||||
contexts *context.ContextTree
|
||||
refHelper *RefsHelper
|
||||
model *types.Model
|
||||
setCommitMessage func(message string)
|
||||
commitsHelper *CommitsHelper
|
||||
gpgHelper *GpgHelper
|
||||
}
|
||||
|
||||
func NewWorkingTreeHelper(
|
||||
@@ -36,16 +37,18 @@ func NewWorkingTreeHelper(
|
||||
refHelper *RefsHelper,
|
||||
model *types.Model,
|
||||
setCommitMessage func(message string),
|
||||
getSavedCommitMessage func() string,
|
||||
commitsHelper *CommitsHelper,
|
||||
gpgHelper *GpgHelper,
|
||||
) *WorkingTreeHelper {
|
||||
return &WorkingTreeHelper{
|
||||
c: c,
|
||||
git: git,
|
||||
contexts: contexts,
|
||||
refHelper: refHelper,
|
||||
model: model,
|
||||
setCommitMessage: setCommitMessage,
|
||||
getSavedCommitMessage: getSavedCommitMessage,
|
||||
c: c,
|
||||
git: git,
|
||||
contexts: contexts,
|
||||
refHelper: refHelper,
|
||||
model: model,
|
||||
setCommitMessage: setCommitMessage,
|
||||
commitsHelper: commitsHelper,
|
||||
gpgHelper: gpgHelper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) HandleCommitPress() error {
|
||||
func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string) error {
|
||||
if err := self.prepareFilesForCommit(); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
@@ -107,28 +110,25 @@ func (self *WorkingTreeHelper) HandleCommitPress() error {
|
||||
return self.PromptToStageAllAndRetry(self.HandleCommitPress)
|
||||
}
|
||||
|
||||
savedCommitMessage := self.getSavedCommitMessage()
|
||||
if len(savedCommitMessage) > 0 {
|
||||
self.setCommitMessage(savedCommitMessage)
|
||||
} else {
|
||||
commitPrefixConfig := self.commitPrefixConfigForRepo()
|
||||
if commitPrefixConfig != nil {
|
||||
prefixPattern := commitPrefixConfig.Pattern
|
||||
prefixReplace := commitPrefixConfig.Replace
|
||||
rgx, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
|
||||
self.setCommitMessage(prefix)
|
||||
}
|
||||
}
|
||||
return self.commitsHelper.OpenCommitMessagePanel(
|
||||
&OpenCommitMessagePanelOpts{
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: initialMessage,
|
||||
Title: self.c.Tr.CommitSummary,
|
||||
PreserveMessage: true,
|
||||
OnConfirm: self.handleCommit,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if err := self.c.PushContext(self.contexts.CommitMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func (self *WorkingTreeHelper) handleCommit(message string) error {
|
||||
cmdObj := self.git.Commit.CommitCmdObj(message)
|
||||
self.c.LogAction(self.c.Tr.Actions.Commit)
|
||||
_ = self.commitsHelper.PopCommitMessageContexts()
|
||||
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
|
||||
self.commitsHelper.OnCommitSuccess()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// HandleCommitEditorPress - handle when the user wants to commit changes via
|
||||
@@ -154,9 +154,27 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
|
||||
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
self.setCommitMessage(skipHookPrefix)
|
||||
return self.HandleCommitPressWithMessage(skipHookPrefix)
|
||||
}
|
||||
|
||||
return self.HandleCommitPress()
|
||||
func (self *WorkingTreeHelper) HandleCommitPress() error {
|
||||
message := self.contexts.CommitMessage.GetPreservedMessage()
|
||||
|
||||
if message != "" {
|
||||
commitPrefixConfig := self.commitPrefixConfigForRepo()
|
||||
if commitPrefixConfig != nil {
|
||||
prefixPattern := commitPrefixConfig.Pattern
|
||||
prefixReplace := commitPrefixConfig.Replace
|
||||
rgx, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
return self.c.ErrorMsg(fmt.Sprintf("%s: %s", self.c.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(self.refHelper.GetCheckedOutRef().Name, prefixReplace)
|
||||
message = prefix
|
||||
}
|
||||
}
|
||||
|
||||
return self.HandleCommitPressWithMessage(message)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) error {
|
||||
|
||||
@@ -3,11 +3,14 @@ package controllers
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -17,7 +20,6 @@ type (
|
||||
type LocalCommitsController struct {
|
||||
baseController
|
||||
*controllerCommon
|
||||
|
||||
pullFiles PullFilesFn
|
||||
}
|
||||
|
||||
@@ -153,7 +155,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
|
||||
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
|
||||
}
|
||||
|
||||
applied, err := self.handleMidRebaseCommand("squash", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Squash, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -167,7 +169,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.SquashCommitDown)
|
||||
return self.interactiveRebase("squash")
|
||||
return self.interactiveRebase(todo.Squash)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -178,7 +180,7 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
|
||||
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
|
||||
}
|
||||
|
||||
applied, err := self.handleMidRebaseCommand("fixup", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Fixup, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,14 +194,14 @@ 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)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("reword", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Reword, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,24 +209,30 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
|
||||
commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
// TODO: use the commit message panel here
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.LcRewordCommit,
|
||||
InitialContent: message,
|
||||
HandleConfirm: func(response string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RewordCommit)
|
||||
if err := self.git.Rebase.RewordCommit(self.model.Commits, self.context().GetSelectedLineIdx(), response); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
return self.helpers.Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
CommitIndex: self.context().GetSelectedLineIdx(),
|
||||
InitialMessage: commitMessage,
|
||||
Title: self.c.Tr.Actions.RewordCommit,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: self.handleReword,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleReword(message string) error {
|
||||
err := self.git.Rebase.RewordCommit(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), message)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.helpers.Commits.OnCommitSuccess()
|
||||
_ = self.helpers.Commits.PopCommitMessageContexts()
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) doRewordEditor() error {
|
||||
@@ -248,7 +256,7 @@ func (self *LocalCommitsController) doRewordEditor() error {
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
|
||||
midRebase, err := self.handleMidRebaseCommand("reword", commit)
|
||||
midRebase, err := self.handleMidRebaseCommand(todo.Reword, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,7 +276,7 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) drop(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("drop", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Drop, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,14 +290,14 @@ 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)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) edit(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("edit", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Edit, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -299,13 +307,13 @@ 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.git.Rebase.InteractiveRebaseBreakAfter(self.model.Commits, self.context().GetSelectedLineIdx())
|
||||
err := self.git.Rebase.EditRebase(commit.Sha)
|
||||
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) pick(commit *models.Commit) error {
|
||||
applied, err := self.handleMidRebaseCommand("pick", commit)
|
||||
applied, err := self.handleMidRebaseCommand(todo.Pick, commit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -318,7 +326,7 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error {
|
||||
return self.pullFiles()
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) interactiveRebase(action string) error {
|
||||
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error {
|
||||
err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action)
|
||||
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
|
||||
}
|
||||
@@ -326,12 +334,12 @@ func (self *LocalCommitsController) interactiveRebase(action string) error {
|
||||
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
|
||||
// commit meaning you are trying to edit the todo file rather than actually
|
||||
// begin a rebase. It then updates the todo file with that action
|
||||
func (self *LocalCommitsController) handleMidRebaseCommand(action string, commit *models.Commit) (bool, error) {
|
||||
func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) {
|
||||
if !commit.IsTODO() {
|
||||
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE {
|
||||
// If we are in a rebase, the only action that is allowed for
|
||||
// non-todo commits is rewording the current head commit
|
||||
if !(action == "reword" && self.isHeadCommit()) {
|
||||
if !(action == todo.Reword && self.isHeadCommit()) {
|
||||
return true, self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
|
||||
}
|
||||
}
|
||||
@@ -343,19 +351,21 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action string, commit
|
||||
// and that means we either unconditionally wait around for the subprocess to ask for
|
||||
// our input or we set a lazygit client as the EDITOR env variable and have it
|
||||
// request us to edit the commit message when prompted.
|
||||
if action == "reword" {
|
||||
if action == todo.Reword {
|
||||
return true, self.c.ErrorMsg(self.c.Tr.LcRewordNotSupported)
|
||||
}
|
||||
|
||||
if allowed := isChangeOfRebaseTodoAllowed(action); !allowed {
|
||||
return true, self.c.ErrorMsg(self.c.Tr.LcChangingThisActionIsNotAllowed)
|
||||
}
|
||||
|
||||
self.c.LogAction("Update rebase TODO")
|
||||
self.c.LogCommand(
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", commit.ShortSha(), action),
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", commit.ShortSha(), action.String()),
|
||||
false,
|
||||
)
|
||||
|
||||
if err := self.git.Rebase.EditRebaseTodo(
|
||||
self.context().GetSelectedLineIdx(), action,
|
||||
); err != nil {
|
||||
if err := self.git.Rebase.EditRebaseTodo(commit, action); err != nil {
|
||||
return false, self.c.Error(err)
|
||||
}
|
||||
|
||||
@@ -383,7 +393,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.MoveCommitDown)
|
||||
self.c.LogCommand(fmt.Sprintf("Moving commit %s down", commit.ShortSha()), false)
|
||||
|
||||
if err := self.git.Rebase.MoveTodoDown(index); err != nil {
|
||||
if err := self.git.Rebase.MoveTodoDown(commit); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.context().MoveSelectedLine(1)
|
||||
@@ -421,7 +431,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
|
||||
false,
|
||||
)
|
||||
|
||||
if err := self.git.Rebase.MoveTodoDown(index - 1); err != nil {
|
||||
if err := self.git.Rebase.MoveTodoUp(self.model.Commits[index]); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.context().MoveSelectedLine(-1)
|
||||
@@ -436,7 +446,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.git.Rebase.MoveCommitDown(self.model.Commits, index-1)
|
||||
err := self.git.Rebase.MoveCommitUp(self.model.Commits, index)
|
||||
if err == nil {
|
||||
self.context().MoveSelectedLine(-1)
|
||||
}
|
||||
@@ -462,7 +472,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.git.Rebase.AmendTo(commit)
|
||||
err := self.git.Rebase.AmendTo(self.model.Commits, self.context().GetView().SelectedLineIdx())
|
||||
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
},
|
||||
@@ -470,6 +480,10 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error {
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error {
|
||||
if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() {
|
||||
return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: "Amend commit attribute",
|
||||
Items: []*types.MenuItem{
|
||||
@@ -760,3 +774,16 @@ func (self *LocalCommitsController) paste() error {
|
||||
func (self *LocalCommitsController) isHeadCommit() bool {
|
||||
return models.IsHeadCommit(self.model.Commits, self.context().GetSelectedLineIdx())
|
||||
}
|
||||
|
||||
func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool {
|
||||
allowedActions := []todo.TodoCommand{
|
||||
todo.Pick,
|
||||
todo.Drop,
|
||||
todo.Edit,
|
||||
todo.Fixup,
|
||||
todo.Squash,
|
||||
todo.Reword,
|
||||
}
|
||||
|
||||
return lo.Contains(allowedActions, action)
|
||||
}
|
||||
|
||||
+10
-18
@@ -4,15 +4,9 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
|
||||
newlineKey, ok := keybindings.GetKey(gui.c.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
textArea.BackSpaceChar()
|
||||
@@ -30,7 +24,7 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch
|
||||
textArea.MoveRightWord()
|
||||
case key == gocui.KeyArrowRight || key == gocui.KeyCtrlF:
|
||||
textArea.MoveCursorRight()
|
||||
case key == newlineKey:
|
||||
case key == gocui.KeyEnter:
|
||||
if allowMultiline {
|
||||
textArea.TypeRune('\n')
|
||||
} else {
|
||||
@@ -66,22 +60,20 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch
|
||||
// we've just copy+pasted the editor from gocui to here so that we can also re-
|
||||
// render the commit message length on each keypress
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true)
|
||||
|
||||
// This function is called again on refresh as part of the general resize popup call,
|
||||
// but we need to call it here so that when we go to render the text area it's not
|
||||
// considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
|
||||
err := gui.resizePopupPanel(v, v.TextArea.GetContent())
|
||||
if err != nil {
|
||||
gui.c.Log.Error(err)
|
||||
}
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
|
||||
v.RenderTextArea()
|
||||
gui.RenderCommitLength()
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true)
|
||||
v.RenderTextArea()
|
||||
gui.RenderCommitLength()
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
|
||||
|
||||
v.RenderTextArea()
|
||||
|
||||
@@ -83,13 +83,13 @@ func (gui *Gui) filesRenderToMain() error {
|
||||
return gui.c.RenderToMainViews(refreshOpts)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSetTextareaTextFn(getView func() *gocui.View) func(string) {
|
||||
func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) {
|
||||
return func(text string) {
|
||||
// using a getView function so that we don't need to worry about when the view is created
|
||||
view := getView()
|
||||
view.ClearTextArea()
|
||||
view.TextArea.TypeString(text)
|
||||
_ = gui.resizePopupPanel(view, view.TextArea.GetContent())
|
||||
gui.resizeCommitMessagePanels()
|
||||
view.RenderTextArea()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +195,6 @@ type GuiRepoState struct {
|
||||
// back in sync with the repo state
|
||||
ViewsSetup bool
|
||||
|
||||
// we store a commit message in this field if we've escaped the commit message
|
||||
// panel without committing or if our commit failed
|
||||
savedCommitMessage string
|
||||
|
||||
ScreenMode WindowMaximisation
|
||||
|
||||
CurrentPopupOpts *types.CreatePopupPanelOpts
|
||||
|
||||
@@ -58,6 +58,10 @@ func (self *guiCommon) PopContext() error {
|
||||
return self.gui.popContext()
|
||||
}
|
||||
|
||||
func (self *guiCommon) RemoveContexts(contexts []types.Context) error {
|
||||
return self.gui.removeContexts(contexts)
|
||||
}
|
||||
|
||||
func (self *guiCommon) CurrentContext() types.Context {
|
||||
return self.gui.currentContext()
|
||||
}
|
||||
@@ -70,6 +74,10 @@ func (self *guiCommon) IsCurrentContext(c types.Context) bool {
|
||||
return self.CurrentContext().GetKey() == c.GetKey()
|
||||
}
|
||||
|
||||
func (self *guiCommon) ActivateContext(context types.Context) error {
|
||||
return self.gui.activateContext(context, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
func (self *guiCommon) GetAppState() *config.AppState {
|
||||
return self.gui.Config.GetAppState()
|
||||
}
|
||||
|
||||
+1
-1
@@ -182,7 +182,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
|
||||
}
|
||||
|
||||
initialContext := gui.currentSideContext()
|
||||
if err := gui.c.PushContext(initialContext); err != nil {
|
||||
if err := gui.c.ActivateContext(initialContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -263,8 +264,9 @@ func displayCommit(
|
||||
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
|
||||
|
||||
actionString := ""
|
||||
if commit.Action != "" {
|
||||
actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
|
||||
if commit.Action != models.ActionNone {
|
||||
todoString := commit.Action.String()
|
||||
actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
@@ -275,6 +277,8 @@ func displayCommit(
|
||||
} else {
|
||||
if len(commit.Tags) > 0 {
|
||||
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
|
||||
} else if commit.ExtraInfo != "" {
|
||||
tagString = style.FgMagenta.SetBold().Sprint("(*)") + " "
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,19 +347,20 @@ func getShaColor(
|
||||
return getBisectStatusColor(bisectStatus)
|
||||
}
|
||||
|
||||
diffed := commit.Sha == diffName
|
||||
diffed := commit.Sha != "" && commit.Sha == diffName
|
||||
shaColor := theme.DefaultTextColor
|
||||
switch commit.Status {
|
||||
case "unpushed":
|
||||
case models.StatusUnpushed:
|
||||
shaColor = style.FgRed
|
||||
case "pushed":
|
||||
case models.StatusPushed:
|
||||
shaColor = style.FgYellow
|
||||
case "merged":
|
||||
case models.StatusMerged:
|
||||
shaColor = style.FgGreen
|
||||
case "rebasing":
|
||||
case models.StatusRebasing:
|
||||
shaColor = style.FgBlue
|
||||
case "reflog":
|
||||
case models.StatusReflog:
|
||||
shaColor = style.FgBlue
|
||||
default:
|
||||
}
|
||||
|
||||
if diffed {
|
||||
@@ -367,15 +372,15 @@ func getShaColor(
|
||||
return shaColor
|
||||
}
|
||||
|
||||
func actionColorMap(str string) style.TextStyle {
|
||||
switch str {
|
||||
case "pick":
|
||||
func actionColorMap(action todo.TodoCommand) style.TextStyle {
|
||||
switch action {
|
||||
case todo.Pick:
|
||||
return style.FgCyan
|
||||
case "drop":
|
||||
case todo.Drop:
|
||||
return style.FgRed
|
||||
case "edit":
|
||||
case todo.Edit:
|
||||
return style.FgGreen
|
||||
case "fixup":
|
||||
case todo.Fixup:
|
||||
return style.FgMagenta
|
||||
default:
|
||||
return style.FgYellow
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
@@ -91,8 +92,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "showing graph, including rebase commits",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: "pick"},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
|
||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||
@@ -114,8 +115,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "showing graph, including rebase commits, with offset",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: "pick"},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
|
||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||
@@ -136,8 +137,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "startIdx is past TODO commits",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: "pick"},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
|
||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||
@@ -156,8 +157,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "only showing TODO commits",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: "pick"},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
|
||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||
@@ -195,10 +196,10 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "only TODO commits except last",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: "pick"},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: "pick"},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}, Action: "pick"},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}, Action: todo.Pick},
|
||||
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}, Action: todo.Pick},
|
||||
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
|
||||
},
|
||||
startIdx: 0,
|
||||
@@ -215,7 +216,7 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
|
||||
{
|
||||
testName: "don't show YOU ARE HERE label when not asked for (e.g. in branches panel)",
|
||||
commits: []*models.Commit{
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2"}, Action: "pick"},
|
||||
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2"}, Action: todo.Pick},
|
||||
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}},
|
||||
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
|
||||
},
|
||||
|
||||
@@ -85,6 +85,7 @@ func (gui *Gui) Refresh(options types.RefreshOptions) error {
|
||||
types.REMOTES,
|
||||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
})
|
||||
} else {
|
||||
scopeSet = set.NewFromSlice(options.Scope)
|
||||
@@ -563,6 +564,13 @@ func (gui *Gui) refreshStatus() {
|
||||
|
||||
func (gui *Gui) refreshStagingPanel(focusOpts types.OnFocusOpts) error {
|
||||
secondaryFocused := gui.secondaryStagingFocused()
|
||||
mainFocused := gui.mainStagingFocused()
|
||||
|
||||
// this method could be called when the staging panel is not being used,
|
||||
// in which case we don't want to do anything.
|
||||
if !mainFocused && !secondaryFocused {
|
||||
return nil
|
||||
}
|
||||
|
||||
mainSelectedLineIdx := -1
|
||||
secondarySelectedLineIdx := -1
|
||||
@@ -647,6 +655,10 @@ func (gui *Gui) secondaryStagingFocused() bool {
|
||||
return gui.currentStaticContext().GetKey() == gui.State.Contexts.StagingSecondary.GetKey()
|
||||
}
|
||||
|
||||
func (gui *Gui) mainStagingFocused() bool {
|
||||
return gui.currentStaticContext().GetKey() == gui.State.Contexts.Staging.GetKey()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(opts types.OnFocusOpts) error {
|
||||
selectedLineIdx := -1
|
||||
if opts.ClickedWindowName == "main" {
|
||||
|
||||
@@ -42,9 +42,14 @@ type IGuiCommon interface {
|
||||
|
||||
PushContext(context Context, opts ...OnFocusOpts) error
|
||||
PopContext() error
|
||||
// Removes all given contexts from the stack. If a given context is not in the stack, it is ignored.
|
||||
// This is for when you have a group of contexts that are bundled together e.g. with the commit message panel.
|
||||
// If you want to remove a single context, you should probably use PopContext instead.
|
||||
RemoveContexts([]Context) error
|
||||
CurrentContext() Context
|
||||
CurrentStaticContext() Context
|
||||
IsCurrentContext(Context) bool
|
||||
ActivateContext(context Context) error
|
||||
// enters search mode for the current view
|
||||
OpenSearch()
|
||||
|
||||
|
||||
+23
-12
@@ -78,28 +78,24 @@ func (gui *Gui) resizeCurrentPopupPanel() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v == gui.Views.Menu {
|
||||
c := gui.c.CurrentContext()
|
||||
|
||||
if c == gui.State.Contexts.Menu {
|
||||
gui.resizeMenu()
|
||||
} else if v == gui.Views.Confirmation || v == gui.Views.Suggestions {
|
||||
} else if c == gui.State.Contexts.Confirmation || c == gui.State.Contexts.Suggestions {
|
||||
gui.resizeConfirmationPanel()
|
||||
} else if gui.isPopupPanel(v.Name()) {
|
||||
return gui.resizePopupPanel(v, v.Buffer())
|
||||
} else if c == gui.State.Contexts.CommitMessage || c == gui.State.Contexts.CommitDescription {
|
||||
gui.resizeCommitMessagePanels()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content)
|
||||
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) resizeMenu() {
|
||||
itemCount := gui.State.Contexts.Menu.GetList().Len()
|
||||
offset := 3
|
||||
panelWidth := gui.getConfirmationPanelWidth()
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
|
||||
x0, y0, x1, y1 := gui.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
|
||||
menuBottom := y1 - offset
|
||||
_, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0)
|
||||
|
||||
@@ -121,7 +117,7 @@ func (gui *Gui) resizeConfirmationPanel() {
|
||||
wrap = false
|
||||
}
|
||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
|
||||
x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight)
|
||||
confirmationViewBottom := y1 - suggestionsViewHeight
|
||||
_, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
|
||||
|
||||
@@ -129,6 +125,21 @@ func (gui *Gui) resizeConfirmationPanel() {
|
||||
_, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
|
||||
}
|
||||
|
||||
func (gui *Gui) resizeCommitMessagePanels() {
|
||||
panelWidth := gui.getConfirmationPanelWidth()
|
||||
content := gui.Views.CommitDescription.TextArea.GetContent()
|
||||
summaryViewHeight := 3
|
||||
panelHeight := gui.getMessageHeight(false, content, panelWidth)
|
||||
minHeight := 7
|
||||
if panelHeight < minHeight {
|
||||
panelHeight = minHeight
|
||||
}
|
||||
x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight)
|
||||
|
||||
_, _ = gui.g.SetView(gui.Views.CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0)
|
||||
_, _ = gui.g.SetView(gui.Views.CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0)
|
||||
}
|
||||
|
||||
func (gui *Gui) globalOptionsMap() map[string]string {
|
||||
keybindingConfig := gui.c.UserConfig.Keybinding
|
||||
|
||||
|
||||
+24
-15
@@ -26,20 +26,21 @@ type Views struct {
|
||||
PatchBuildingSecondary *gocui.View
|
||||
MergeConflicts *gocui.View
|
||||
|
||||
Options *gocui.View
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
CommitMessage *gocui.View
|
||||
CommitFiles *gocui.View
|
||||
SubCommits *gocui.View
|
||||
Information *gocui.View
|
||||
AppStatus *gocui.View
|
||||
Search *gocui.View
|
||||
SearchPrefix *gocui.View
|
||||
Limit *gocui.View
|
||||
Suggestions *gocui.View
|
||||
Tooltip *gocui.View
|
||||
Extras *gocui.View
|
||||
Options *gocui.View
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
CommitMessage *gocui.View
|
||||
CommitDescription *gocui.View
|
||||
CommitFiles *gocui.View
|
||||
SubCommits *gocui.View
|
||||
Information *gocui.View
|
||||
AppStatus *gocui.View
|
||||
Search *gocui.View
|
||||
SearchPrefix *gocui.View
|
||||
Limit *gocui.View
|
||||
Suggestions *gocui.View
|
||||
Tooltip *gocui.View
|
||||
Extras *gocui.View
|
||||
|
||||
// for playing the easter egg snake game
|
||||
Snake *gocui.View
|
||||
@@ -94,6 +95,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
|
||||
// popups.
|
||||
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
|
||||
{viewPtr: &gui.Views.CommitDescription, name: "commitDescription"},
|
||||
{viewPtr: &gui.Views.Menu, name: "menu"},
|
||||
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
|
||||
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
|
||||
@@ -199,10 +201,17 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.AppStatus.Frame = false
|
||||
|
||||
gui.Views.CommitMessage.Visible = false
|
||||
gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage
|
||||
gui.Views.CommitMessage.Title = gui.c.Tr.CommitSummary
|
||||
gui.Views.CommitMessage.Editable = true
|
||||
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
|
||||
gui.Views.CommitDescription.Visible = false
|
||||
gui.Views.CommitDescription.Title = gui.c.Tr.CommitDescriptionTitle
|
||||
gui.Views.CommitDescription.Subtitle = gui.Tr.CommitDescriptionSubTitle
|
||||
gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitDescription.Editable = true
|
||||
gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor)
|
||||
|
||||
gui.Views.Confirmation.Visible = false
|
||||
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
+2
-4
@@ -47,7 +47,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
StagingTitle: "正在暂存",
|
||||
MergingTitle: "正在合并",
|
||||
NormalTitle: "正常",
|
||||
CommitMessage: "提交信息",
|
||||
CommitSummary: "提交信息",
|
||||
CredentialsUsername: "用户名",
|
||||
CredentialsPassword: "密码",
|
||||
CredentialsPassphrase: "输入 SSH 密钥的密码",
|
||||
@@ -94,7 +94,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
LcNewBranch: "新分支",
|
||||
LcDeleteBranch: "删除分支",
|
||||
NoBranchesThisRepo: "此仓库中没有分支",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindNewLine}}:新行,{{.keyBindConfirm}}:确认",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认",
|
||||
CommitWithoutMessageErr: "您必须编写提交消息才能进行提交",
|
||||
CloseConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认",
|
||||
LcClose: "关闭",
|
||||
@@ -201,8 +201,6 @@ func chineseTranslationSet() TranslationSet {
|
||||
ReflogCommitsTitle: "Reflog 页面",
|
||||
GlobalTitle: "全局键绑定",
|
||||
ConflictsResolved: "已解决所有冲突。是否继续?",
|
||||
RebasingTitle: "变基",
|
||||
ConfirmRebase: "您确定要将分支 {{.checkedOutBranch}} 变基到 {{.selectedBranch}} 吗?",
|
||||
ConfirmMerge: "您确定要将分支 {{.selectedBranch}} 合并到 {{.checkedOutBranch}} 吗?",
|
||||
FwdNoUpstream: "此分支没有上游,无法快进",
|
||||
FwdNoLocalUpstream: "此分支的远程未在本地注册,无法快进",
|
||||
|
||||
+2
-4
@@ -13,7 +13,7 @@ func dutchTranslationSet() TranslationSet {
|
||||
MainTitle: "Hoofd",
|
||||
StagingTitle: "Staging",
|
||||
NormalTitle: "Normaal",
|
||||
CommitMessage: "Commitbericht",
|
||||
CommitSummary: "Commitbericht",
|
||||
CredentialsUsername: "Gebruikersnaam",
|
||||
CredentialsPassword: "Wachtwoord",
|
||||
CredentialsPassphrase: "Voer een wachtwoordzin in voor de SSH-sleutel",
|
||||
@@ -60,7 +60,7 @@ func dutchTranslationSet() TranslationSet {
|
||||
LcNewBranch: "nieuwe branch",
|
||||
LcDeleteBranch: "verwijder branch",
|
||||
NoBranchesThisRepo: "Geen branches voor deze repo",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindNewLine}}: Nieuwe lijn, {{.keyBindConfirm}}: Bevestig",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig",
|
||||
CommitWithoutMessageErr: "Je kan geen commit maken zonder commit bericht",
|
||||
CloseConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig",
|
||||
LcClose: "sluiten",
|
||||
@@ -166,9 +166,7 @@ func dutchTranslationSet() TranslationSet {
|
||||
ReflogCommitsTitle: "Reflog",
|
||||
GlobalTitle: "Globale Sneltoetsen",
|
||||
ConflictsResolved: "alle merge conflicten zijn opgelost. Wilt je verder gaan?",
|
||||
RebasingTitle: "Rebasen",
|
||||
MergingTitle: "Mergen",
|
||||
ConfirmRebase: "Weet je zeker dat je '{{.checkedOutBranch}}' op '{{.selectedBranch}}' wil rebasen?",
|
||||
ConfirmMerge: "Weet je zeker dat je '{{.selectedBranch}}' in '{{.checkedOutBranch}}' wil mergen?",
|
||||
FwdNoUpstream: "Kan niet de branch vooruitspoelen zonder upstream",
|
||||
FwdCommitsToPush: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
|
||||
|
||||
+19
-7
@@ -27,7 +27,7 @@ type TranslationSet struct {
|
||||
MergeConfirmTitle string
|
||||
NormalTitle string
|
||||
LogTitle string
|
||||
CommitMessage string
|
||||
CommitSummary string
|
||||
CredentialsUsername string
|
||||
CredentialsPassword string
|
||||
CredentialsPassphrase string
|
||||
@@ -105,6 +105,7 @@ type TranslationSet struct {
|
||||
SureResetCommitAuthor string
|
||||
LcRenameCommitEditor string
|
||||
NoCommitsThisBranch string
|
||||
UpdateRefHere string
|
||||
Error string
|
||||
LcSelectHunk string
|
||||
LcNavigateConflicts string
|
||||
@@ -196,6 +197,8 @@ type TranslationSet struct {
|
||||
MergeOptionsTitle string
|
||||
RebaseOptionsTitle string
|
||||
CommitMessageTitle string
|
||||
CommitDescriptionTitle string
|
||||
CommitDescriptionSubTitle string
|
||||
LocalBranchesTitle string
|
||||
SearchTitle string
|
||||
TagsTitle string
|
||||
@@ -208,7 +211,9 @@ type TranslationSet struct {
|
||||
ReflogCommitsTitle string
|
||||
ConflictsResolved string
|
||||
RebasingTitle string
|
||||
ConfirmRebase string
|
||||
SimpleRebase string
|
||||
InteractiveRebase string
|
||||
InteractiveRebaseTooltip string
|
||||
ConfirmMerge string
|
||||
FwdNoUpstream string
|
||||
FwdNoLocalUpstream string
|
||||
@@ -218,6 +223,7 @@ type TranslationSet struct {
|
||||
YouAreHere string
|
||||
YouDied string
|
||||
LcRewordNotSupported string
|
||||
LcChangingThisActionIsNotAllowed string
|
||||
LcCherryPickCopy string
|
||||
LcCherryPickCopyRange string
|
||||
LcPasteCommits string
|
||||
@@ -694,7 +700,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
MergingTitle: "Main Panel (Merging)",
|
||||
NormalTitle: "Main Panel (Normal)",
|
||||
LogTitle: "Log",
|
||||
CommitMessage: "Commit message",
|
||||
CommitSummary: "Commit summary",
|
||||
CredentialsUsername: "Username",
|
||||
CredentialsPassword: "Password",
|
||||
CredentialsPassphrase: "Enter passphrase for SSH key",
|
||||
@@ -746,7 +752,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
LcNewBranch: "new branch",
|
||||
LcDeleteBranch: "delete branch",
|
||||
NoBranchesThisRepo: "No branches for this repo",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
|
||||
CommitWithoutMessageErr: "You cannot commit without a commit message",
|
||||
CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm",
|
||||
LcClose: "close",
|
||||
@@ -754,6 +760,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
LcSquashDown: "squash down",
|
||||
LcFixupCommit: "fixup commit",
|
||||
NoCommitsThisBranch: "No commits for this branch",
|
||||
UpdateRefHere: "Update branch '{{.ref}}' here",
|
||||
CannotSquashOrFixupFirstCommit: "There's no commit below to squash into",
|
||||
Fixup: "Fixup",
|
||||
SureFixupThisCommit: "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
|
||||
@@ -861,7 +868,9 @@ func EnglishTranslationSet() TranslationSet {
|
||||
RecentRepos: "recent repositories",
|
||||
MergeOptionsTitle: "Merge Options",
|
||||
RebaseOptionsTitle: "Rebase Options",
|
||||
CommitMessageTitle: "Commit Message",
|
||||
CommitMessageTitle: "Commit Summary",
|
||||
CommitDescriptionTitle: "Commit description",
|
||||
CommitDescriptionSubTitle: "Press tab to toggle focus",
|
||||
LocalBranchesTitle: "Local Branches",
|
||||
SearchTitle: "Search",
|
||||
TagsTitle: "Tags",
|
||||
@@ -874,8 +883,10 @@ func EnglishTranslationSet() TranslationSet {
|
||||
ReflogCommitsTitle: "Reflog",
|
||||
GlobalTitle: "Global Keybindings",
|
||||
ConflictsResolved: "all merge conflicts resolved. Continue?",
|
||||
RebasingTitle: "Rebasing",
|
||||
ConfirmRebase: "Are you sure you want to rebase '{{.checkedOutBranch}}' on top of '{{.selectedBranch}}'?",
|
||||
RebasingTitle: "Rebase '{{.checkedOutBranch}}' onto '{{.ref}}'",
|
||||
SimpleRebase: "simple rebase",
|
||||
InteractiveRebase: "interactive rebase",
|
||||
InteractiveRebaseTooltip: "Begin an interactive rebase with a break at the start, so you can update the TODO commits before continuing",
|
||||
ConfirmMerge: "Are you sure you want to merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'?",
|
||||
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
|
||||
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
|
||||
@@ -885,6 +896,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
YouAreHere: "YOU ARE HERE",
|
||||
YouDied: "YOU DIED!",
|
||||
LcRewordNotSupported: "rewording commits while interactively rebasing is not currently supported",
|
||||
LcChangingThisActionIsNotAllowed: "changing this kind of rebase todo entry is not allowed",
|
||||
LcCherryPickCopy: "copy commit (cherry-pick)",
|
||||
LcCherryPickCopyRange: "copy commit range (cherry-pick)",
|
||||
LcPasteCommits: "paste commits (cherry-pick)",
|
||||
|
||||
@@ -34,7 +34,7 @@ func japaneseTranslationSet() TranslationSet {
|
||||
MergingTitle: "メインパネル (Merging)",
|
||||
NormalTitle: "メインパネル (Normal)",
|
||||
LogTitle: "ログ",
|
||||
CommitMessage: "コミットメッセージ",
|
||||
CommitSummary: "コミットメッセージ",
|
||||
CredentialsUsername: "ユーザ名",
|
||||
CredentialsPassword: "パスワード",
|
||||
CredentialsPassphrase: "SSH鍵のパスフレーズを入力",
|
||||
@@ -85,7 +85,7 @@ func japaneseTranslationSet() TranslationSet {
|
||||
LcNewBranch: "新しいブランチを作成",
|
||||
LcDeleteBranch: "ブランチを削除",
|
||||
NoBranchesThisRepo: "リポジトリにブランチが存在しません",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindNewLine}}: 改行, {{.keyBindConfirm}}: 確定",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindConfirm}}: 確定",
|
||||
CommitWithoutMessageErr: "コミットメッセージを入力してください",
|
||||
CloseConfirm: "{{.keyBindClose}}: 閉じる/キャンセル, {{.keyBindConfirm}}: 確認",
|
||||
LcClose: "閉じる",
|
||||
|
||||
+2
-4
@@ -33,7 +33,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
MergingTitle: "메인 패널 (Merging)",
|
||||
NormalTitle: "메인 패널 (Normal)",
|
||||
LogTitle: "로그",
|
||||
CommitMessage: "커밋 메시지",
|
||||
CommitSummary: "커밋 메시지",
|
||||
CredentialsUsername: "사용자 이름",
|
||||
CredentialsPassword: "패스워드",
|
||||
CredentialsPassphrase: "SSH키의 passphrase 입력",
|
||||
@@ -84,7 +84,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
LcNewBranch: "새 브랜치 생성",
|
||||
LcDeleteBranch: "브랜치 삭제",
|
||||
NoBranchesThisRepo: "저장소에 브랜치가 존재하지 않습니다.",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindNewLine}}: 개행, {{.keyBindConfirm}}: 확인",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindConfirm}}: 확인",
|
||||
CommitWithoutMessageErr: "커밋 메시지를 입력하세요.",
|
||||
CloseConfirm: "{{.keyBindClose}}: 닫기/취소, {{.keyBindConfirm}}: 확인",
|
||||
LcClose: "닫기",
|
||||
@@ -202,8 +202,6 @@ func koreanTranslationSet() TranslationSet {
|
||||
ReflogCommitsTitle: "Reflog",
|
||||
GlobalTitle: "글로벌 키 바인딩",
|
||||
ConflictsResolved: "모든 병합 충돌이 해결되었습니다. 계속 할까요?",
|
||||
RebasingTitle: "리베이스 중",
|
||||
ConfirmRebase: "정말로 '{{.checkedOutBranch}}' 을(를) '{{.selectedBranch}}'에 리베이스 하시겠습니까?",
|
||||
ConfirmMerge: "정말로 '{{.selectedBranch}}' 을(를) '{{.checkedOutBranch}}'에 병합하시겠습니까?",
|
||||
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
|
||||
FwdNoLocalUpstream: "Cannot fast-forward a branch whose remote is not registered locally",
|
||||
|
||||
+2
-4
@@ -10,7 +10,7 @@ func polishTranslationSet() TranslationSet {
|
||||
StashTitle: "Schowek",
|
||||
UnstagedChanges: "Zmiany poza poczekalnią",
|
||||
StagedChanges: "Zmiany w poczekalni",
|
||||
CommitMessage: "Komunikat commita",
|
||||
CommitSummary: "Komunikat commita",
|
||||
CredentialsUsername: "Użytkownik",
|
||||
CredentialsPassword: "Hasło",
|
||||
CredentialsPassphrase: "Fraza",
|
||||
@@ -55,7 +55,7 @@ func polishTranslationSet() TranslationSet {
|
||||
LcNewBranch: "nowa gałąź",
|
||||
LcDeleteBranch: "usuń gałąź",
|
||||
NoBranchesThisRepo: "Brak gałęzi dla tego repozytorium",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindNewLine}}: nowa linia, {{.keyBindConfirm}}: potwierdź",
|
||||
CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
|
||||
CommitWithoutMessageErr: "Nie możesz commitować bez komunikatu",
|
||||
CloseConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
|
||||
LcClose: "zamknij",
|
||||
@@ -112,9 +112,7 @@ func polishTranslationSet() TranslationSet {
|
||||
FileStagingRequirements: "Można tylko zatwierdzić pojedyncze linie dla śledzonych plików z niezatwierdzonymi zmianami",
|
||||
StagingTitle: "Poczekalnia",
|
||||
ReturnToFilesPanel: "wróć do panelu plików",
|
||||
RebasingTitle: "Zmiana bazy",
|
||||
MergingTitle: "Scalanie",
|
||||
ConfirmRebase: "Czy napewno chcesz zmienić bazę '{{.checkedOutBranch}}' na '{{.selectedBranch}}'?",
|
||||
ConfirmMerge: "Czy na pewno chcesz scalić '{{.selectedBranch}}' do '{{.checkedOutBranch}}'?",
|
||||
FwdNoUpstream: "Nie można przewinąć gałęzi bez gałęzi nadrzędnej",
|
||||
FwdCommitsToPush: "Nie można przewinąć gałęzi z commitami do wysłania",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package components
|
||||
|
||||
type CommitDescriptionPanelDriver struct {
|
||||
t *TestDriver
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) getViewDriver() *ViewDriver {
|
||||
return self.t.Views().CommitDescription()
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) Type(value string) *CommitDescriptionPanelDriver {
|
||||
self.t.typeContent(value)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) SwitchToSummary() *CommitMessagePanelDriver {
|
||||
self.getViewDriver().PressTab()
|
||||
return &CommitMessagePanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDriver {
|
||||
self.t.press(self.t.keys.Universal.Confirm)
|
||||
return self
|
||||
}
|
||||
@@ -10,19 +10,36 @@ func (self *CommitMessagePanelDriver) getViewDriver() *ViewDriver {
|
||||
|
||||
// asserts on the text initially present in the prompt
|
||||
func (self *CommitMessagePanelDriver) InitialText(expected *Matcher) *CommitMessagePanelDriver {
|
||||
return self.Content(expected)
|
||||
}
|
||||
|
||||
// asserts on the current context in the prompt
|
||||
func (self *CommitMessagePanelDriver) Content(expected *Matcher) *CommitMessagePanelDriver {
|
||||
self.getViewDriver().Content(expected)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
// asserts that the confirmation view has the expected title
|
||||
func (self *CommitMessagePanelDriver) Title(expected *Matcher) *CommitMessagePanelDriver {
|
||||
self.getViewDriver().Title(expected)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) Type(value string) *CommitMessagePanelDriver {
|
||||
self.t.typeContent(value)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) SwitchToDescription() *CommitDescriptionPanelDriver {
|
||||
self.getViewDriver().PressTab()
|
||||
return &CommitDescriptionPanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) AddNewline() *CommitMessagePanelDriver {
|
||||
self.t.press(self.t.keys.Universal.AppendNewline)
|
||||
self.t.press(self.t.keys.Universal.Confirm)
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -49,6 +66,20 @@ func (self *CommitMessagePanelDriver) Confirm() {
|
||||
self.getViewDriver().PressEnter()
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) Close() {
|
||||
self.getViewDriver().PressEscape()
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) Cancel() {
|
||||
self.getViewDriver().PressEscape()
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) SelectPreviousMessage() *CommitMessagePanelDriver {
|
||||
self.getViewDriver().SelectPreviousItem()
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitMessagePanelDriver) SelectNextMessage() *CommitMessagePanelDriver {
|
||||
self.getViewDriver().SelectNextItem()
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -62,9 +62,22 @@ func (self *Popup) CommitMessagePanel() *CommitMessagePanelDriver {
|
||||
return &CommitMessagePanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *Popup) CommitDescriptionPanel() *CommitMessagePanelDriver {
|
||||
self.inCommitDescriptionPanel()
|
||||
|
||||
return &CommitMessagePanelDriver{t: self.t}
|
||||
}
|
||||
|
||||
func (self *Popup) inCommitMessagePanel() {
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
currentView := self.t.gui.CurrentContext().GetView()
|
||||
return currentView.Name() == "commitMessage", "Expected commit message panel to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Popup) inCommitDescriptionPanel() {
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
currentView := self.t.gui.CurrentContext().GetView()
|
||||
return currentView.Name() == "commitDescription", "Expected commit description panel to be focused"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,11 @@ func RunTests(
|
||||
return err
|
||||
}
|
||||
|
||||
gitVersion, err := getGitVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
@@ -51,7 +57,7 @@ func RunTests(
|
||||
)
|
||||
|
||||
for i := 0; i < maxAttempts; i++ {
|
||||
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, keyPressDelay)
|
||||
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, keyPressDelay, gitVersion)
|
||||
if err != nil {
|
||||
if i == maxAttempts-1 {
|
||||
return err
|
||||
@@ -77,12 +83,18 @@ func runTest(
|
||||
runCmd func(cmd *exec.Cmd) error,
|
||||
sandbox bool,
|
||||
keyPressDelay int,
|
||||
gitVersion *git_commands.GitVersion,
|
||||
) error {
|
||||
if test.Skip() {
|
||||
logf("Skipping test %s", test.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
if !test.ShouldRunForGitVersion(gitVersion) {
|
||||
logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
|
||||
return nil
|
||||
}
|
||||
|
||||
logf("path: %s", paths.Root())
|
||||
|
||||
if err := prepareTestDir(test, paths, projectRootDir); err != nil {
|
||||
@@ -144,6 +156,16 @@ func globalGitConfigPath(rootDir string) string {
|
||||
return filepath.Join(rootDir, "test", "global_git_config")
|
||||
}
|
||||
|
||||
func getGitVersion() (*git_commands.GitVersion, error) {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
cmdObj := osCommand.Cmd.New("git --version")
|
||||
versionStr, err := cmdObj.RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return git_commands.ParseGitVersion(versionStr)
|
||||
}
|
||||
|
||||
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, keyPressDelay int) (*exec.Cmd, error) {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
|
||||
@@ -192,7 +192,11 @@ func (self *Shell) DeleteFileAndAdd(fileName string) *Shell {
|
||||
// The reason for padding with zeroes is so that it's easier to do string
|
||||
// matches on the commit messages when there are many of them
|
||||
func (self *Shell) CreateNCommits(n int) *Shell {
|
||||
for i := 1; i <= n; i++ {
|
||||
return self.CreateNCommitsStartingAt(n, 1)
|
||||
}
|
||||
|
||||
func (self *Shell) CreateNCommitsStartingAt(n, startIndex int) *Shell {
|
||||
for i := startIndex; i < startIndex+n; i++ {
|
||||
self.CreateFileAndAdd(
|
||||
fmt.Sprintf("file%02d.txt", i),
|
||||
fmt.Sprintf("file%02d content", i),
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
@@ -28,6 +30,7 @@ type IntegrationTest struct {
|
||||
testDriver *TestDriver,
|
||||
keys config.KeybindingConfig,
|
||||
)
|
||||
gitVersion GitVersionRestriction
|
||||
}
|
||||
|
||||
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
|
||||
@@ -45,6 +48,56 @@ type NewIntegrationTestArgs struct {
|
||||
ExtraCmdArgs string
|
||||
// for when a test is flakey
|
||||
Skip bool
|
||||
// to run a test only on certain git versions
|
||||
GitVersion GitVersionRestriction
|
||||
}
|
||||
|
||||
type GitVersionRestriction struct {
|
||||
// Only one of these fields can be non-empty; use functions below to construct
|
||||
from string
|
||||
before string
|
||||
includes []string
|
||||
}
|
||||
|
||||
// Verifies the version is at least the given version (inclusive)
|
||||
func From(version string) GitVersionRestriction {
|
||||
return GitVersionRestriction{from: version}
|
||||
}
|
||||
|
||||
// Verifies the version is before the given version (exclusive)
|
||||
func Before(version string) GitVersionRestriction {
|
||||
return GitVersionRestriction{before: version}
|
||||
}
|
||||
|
||||
func Includes(versions ...string) GitVersionRestriction {
|
||||
return GitVersionRestriction{includes: versions}
|
||||
}
|
||||
|
||||
func (self GitVersionRestriction) shouldRunOnVersion(version *git_commands.GitVersion) bool {
|
||||
if self.from != "" {
|
||||
from, err := git_commands.ParseGitVersion(self.from)
|
||||
if err != nil {
|
||||
panic("Invalid git version string: " + self.from)
|
||||
}
|
||||
return !version.IsOlderThanVersion(from)
|
||||
}
|
||||
if self.before != "" {
|
||||
before, err := git_commands.ParseGitVersion(self.before)
|
||||
if err != nil {
|
||||
panic("Invalid git version string: " + self.before)
|
||||
}
|
||||
return version.IsOlderThanVersion(before)
|
||||
}
|
||||
if len(self.includes) != 0 {
|
||||
return slices.Some(self.includes, func(str string) bool {
|
||||
v, err := git_commands.ParseGitVersion(str)
|
||||
if err != nil {
|
||||
panic("Invalid git version string: " + str)
|
||||
}
|
||||
return version.Major == v.Major && version.Minor == v.Minor && version.Patch == v.Patch
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
@@ -63,6 +116,7 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
setupRepo: args.SetupRepo,
|
||||
setupConfig: args.SetupConfig,
|
||||
run: args.Run,
|
||||
gitVersion: args.GitVersion,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +136,10 @@ func (self *IntegrationTest) Skip() bool {
|
||||
return self.skip
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) ShouldRunForGitVersion(version *git_commands.GitVersion) bool {
|
||||
return self.gitVersion.shouldRunOnVersion(version)
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) SetupConfig(config *config.AppConfig) {
|
||||
self.setupConfig(config)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
@@ -87,3 +88,65 @@ func TestSuccess(t *testing.T) {
|
||||
assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys)
|
||||
assert.Equal(t, "", driver.failureMessage)
|
||||
}
|
||||
|
||||
func TestGitVersionRestriction(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
gitVersion GitVersionRestriction
|
||||
expectedShouldRun bool
|
||||
}{
|
||||
{
|
||||
testName: "From, current is newer",
|
||||
gitVersion: From("2.24.9"),
|
||||
expectedShouldRun: true,
|
||||
},
|
||||
{
|
||||
testName: "From, current is same",
|
||||
gitVersion: From("2.25.0"),
|
||||
expectedShouldRun: true,
|
||||
},
|
||||
{
|
||||
testName: "From, current is older",
|
||||
gitVersion: From("2.26.0"),
|
||||
expectedShouldRun: false,
|
||||
},
|
||||
{
|
||||
testName: "Before, current is older",
|
||||
gitVersion: Before("2.24.9"),
|
||||
expectedShouldRun: false,
|
||||
},
|
||||
{
|
||||
testName: "Before, current is same",
|
||||
gitVersion: Before("2.25.0"),
|
||||
expectedShouldRun: false,
|
||||
},
|
||||
{
|
||||
testName: "Before, current is newer",
|
||||
gitVersion: Before("2.26.0"),
|
||||
expectedShouldRun: true,
|
||||
},
|
||||
{
|
||||
testName: "Includes, current is included",
|
||||
gitVersion: Includes("2.23.0", "2.25.0"),
|
||||
expectedShouldRun: true,
|
||||
},
|
||||
{
|
||||
testName: "Includes, current is not included",
|
||||
gitVersion: Includes("2.23.0", "2.27.0"),
|
||||
expectedShouldRun: false,
|
||||
},
|
||||
}
|
||||
|
||||
currentGitVersion := git_commands.GitVersion{Major: 2, Minor: 25, Patch: 0}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
GitVersion: s.gitVersion,
|
||||
})
|
||||
shouldRun := test.ShouldRunForGitVersion(¤tGitVersion)
|
||||
assert.Equal(t, shouldRun, s.expectedShouldRun)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,11 @@ func (self *ViewDriver) PressEnter() *ViewDriver {
|
||||
return self.Press(self.t.keys.Universal.Confirm)
|
||||
}
|
||||
|
||||
// i.e. pressing tab
|
||||
func (self *ViewDriver) PressTab() *ViewDriver {
|
||||
return self.Press(self.t.keys.Universal.TogglePanel)
|
||||
}
|
||||
|
||||
// i.e. pressing escape
|
||||
func (self *ViewDriver) PressEscape() *ViewDriver {
|
||||
return self.Press(self.t.keys.Universal.Return)
|
||||
@@ -207,6 +207,10 @@ func (self *Views) CommitMessage() *ViewDriver {
|
||||
return self.regularView("commitMessage")
|
||||
}
|
||||
|
||||
func (self *Views) CommitDescription() *ViewDriver {
|
||||
return self.regularView("commitDescription")
|
||||
}
|
||||
|
||||
func (self *Views) Suggestions() *ViewDriver {
|
||||
return self.regularView("suggestions")
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
Press(keys.Branches.RebaseBranch)
|
||||
|
||||
t.ExpectPopup().Confirmation().
|
||||
Title(Equals("Rebasing")).
|
||||
Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")).
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
|
||||
Select(Contains("simple rebase")).
|
||||
Confirm()
|
||||
|
||||
t.Common().AcknowledgeConflicts()
|
||||
|
||||
@@ -36,9 +36,9 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
Press(keys.Branches.RebaseBranch)
|
||||
|
||||
t.ExpectPopup().Confirmation().
|
||||
Title(Equals("Rebasing")).
|
||||
Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")).
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
|
||||
Select(Contains("simple rebase")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Information().Content(Contains("rebasing"))
|
||||
|
||||
@@ -39,9 +39,9 @@ var RebaseDoesNotAutosquash = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
Press(keys.Branches.RebaseBranch)
|
||||
|
||||
t.ExpectPopup().Confirmation().
|
||||
Title(Equals("Rebasing")).
|
||||
Content(Contains("Are you sure you want to rebase 'my-branch' on top of 'master'?")).
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Rebase 'my-branch' onto 'master'")).
|
||||
Select(Contains("simple rebase")).
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().Lines(
|
||||
|
||||
@@ -22,14 +22,20 @@ var CommitMultiline = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
PressPrimaryAction().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().Type("first line").AddNewline().AddNewline().Type("third line").Confirm()
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Type("first line").
|
||||
SwitchToDescription().
|
||||
AddNewline().
|
||||
AddNewline().
|
||||
Type("fourth line").
|
||||
SwitchToSummary().
|
||||
Confirm()
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("first line"),
|
||||
)
|
||||
|
||||
t.Views().Commits().Focus()
|
||||
t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*third line"))
|
||||
t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*fourth line"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var History = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Cycling through commit message history in the commit message panel",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
shell.EmptyCommit("commit 2")
|
||||
shell.EmptyCommit("commit 3")
|
||||
|
||||
shell.CreateFile("myfile", "myfile content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
PressPrimaryAction(). // stage file
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
InitialText(Equals("")).
|
||||
Type("my commit message").
|
||||
SelectPreviousMessage().
|
||||
Content(Equals("commit 3")).
|
||||
SelectPreviousMessage().
|
||||
Content(Equals("commit 2")).
|
||||
SelectPreviousMessage().
|
||||
Content(Equals("initial commit")).
|
||||
SelectPreviousMessage().
|
||||
Content(Equals("initial commit")). // we hit the end
|
||||
SelectNextMessage().
|
||||
Content(Equals("commit 2")).
|
||||
SelectNextMessage().
|
||||
Content(Equals("commit 3")).
|
||||
SelectNextMessage().
|
||||
Content(Equals("my commit message")).
|
||||
SelectNextMessage().
|
||||
Content(Equals("my commit message")). // we hit the beginning
|
||||
Type(" with extra added").
|
||||
Confirm()
|
||||
|
||||
t.Views().Commits().
|
||||
TopLines(
|
||||
Contains("my commit message with extra added").IsSelected(),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var HistoryComplex = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "More complex flow for cycling commit message history",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
shell.EmptyCommit("commit 2")
|
||||
shell.EmptyCommit("commit 3")
|
||||
|
||||
shell.CreateFileAndAdd("myfile", "myfile content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
// We're going to start a new commit message,
|
||||
// then leave and try to reword a commit, then
|
||||
// come back to original message and confirm we haven't lost our message.
|
||||
// This shows that we're storing the preserved message for a new commit separately
|
||||
// to the message when cycling history.
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
InitialText(Equals("")).
|
||||
Type("my commit message").
|
||||
Cancel()
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
SelectedLine(Contains("commit 3")).
|
||||
Press(keys.Commits.RenameCommit)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
InitialText(Equals("commit 3")).
|
||||
SelectNextMessage().
|
||||
Content(Equals("")).
|
||||
Type("reworded message").
|
||||
SelectPreviousMessage().
|
||||
Content(Equals("commit 3")).
|
||||
SelectNextMessage().
|
||||
Content(Equals("reworded message")).
|
||||
Cancel()
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
InitialText(Equals("my commit message"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Reword = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Staging a couple files and committing",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFile("myfile", "myfile content")
|
||||
shell.CreateFile("myfile2", "myfile2 content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
IsEmpty()
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
PressPrimaryAction().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
commitMessage := "my commit message"
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().Type(commitMessage).Confirm()
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains(commitMessage),
|
||||
)
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
PressPrimaryAction().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
wipCommitMessage := "my commit message wip"
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().Type(wipCommitMessage).Close()
|
||||
|
||||
t.Views().Commits().Focus().
|
||||
Lines(
|
||||
Contains(commitMessage),
|
||||
).Press(keys.Commits.RenameCommit)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
SwitchToDescription().
|
||||
Type("some description").
|
||||
SwitchToSummary().
|
||||
Confirm()
|
||||
|
||||
t.Views().Main().Content(MatchesRegexp("my commit message\n\\s*some description"))
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().Confirm()
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains(wipCommitMessage),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -25,6 +25,13 @@ var UndoChooseHunk = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.Views().MergeConflicts().
|
||||
IsFocused().
|
||||
Content(Contains("<<<<<<< HEAD\nFirst Change")).
|
||||
// explicitly asserting on the selection because sometimes the content renders
|
||||
// before the selection is ready for user input
|
||||
SelectedLines(
|
||||
Contains("<<<<<<< HEAD"),
|
||||
Contains("First Change"),
|
||||
Contains("======="),
|
||||
).
|
||||
PressPrimaryAction().
|
||||
// choosing the first hunk
|
||||
Content(DoesNotContain("<<<<<<< HEAD\nFirst Change")).
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
const (
|
||||
BASE_BRANCH = "base-branch"
|
||||
TOP_BRANCH = "top-branch"
|
||||
BASE_COMMIT = "base-commit"
|
||||
TOP_COMMIT = "top-commit"
|
||||
)
|
||||
|
||||
var AdvancedInteractiveRebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "It begins an interactive rebase and verifies to have the possibility of editing the commits of the branch before proceeding with the actual rebase",
|
||||
ExtraCmdArgs: "",
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
NewBranch(BASE_BRANCH).
|
||||
EmptyCommit(BASE_COMMIT).
|
||||
NewBranch(TOP_BRANCH).
|
||||
EmptyCommit(TOP_COMMIT)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains(TOP_COMMIT),
|
||||
Contains(BASE_COMMIT),
|
||||
)
|
||||
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
NavigateToLine(Contains(BASE_BRANCH)).
|
||||
Press(keys.Branches.RebaseBranch)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals(fmt.Sprintf("Rebase '%s' onto '%s'", TOP_BRANCH, BASE_BRANCH))).
|
||||
Select(Contains("interactive rebase")).
|
||||
Confirm()
|
||||
t.Views().Commits().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains(TOP_COMMIT),
|
||||
Contains(BASE_COMMIT).Contains("YOU ARE HERE"),
|
||||
).
|
||||
NavigateToLine(Contains(TOP_COMMIT)).
|
||||
Press(keys.Universal.Edit).
|
||||
Lines(
|
||||
Contains(TOP_COMMIT).Contains("edit"),
|
||||
Contains(BASE_COMMIT).Contains("YOU ARE HERE"),
|
||||
).
|
||||
Tap(func() {
|
||||
t.Common().ContinueRebase()
|
||||
}).
|
||||
Lines(
|
||||
Contains(TOP_COMMIT).Contains("YOU ARE HERE"),
|
||||
Contains(BASE_COMMIT),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
GitVersion: From("2.38.0"),
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
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"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -27,8 +27,8 @@ var RewordFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("commit 01")).
|
||||
Press(keys.Commits.RenameCommit).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("reword commit")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Reword commit")).
|
||||
InitialText(Equals("commit 01")).
|
||||
Clear().
|
||||
Type("renamed 01").
|
||||
|
||||
@@ -23,8 +23,8 @@ var RewordLastCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Commits.RenameCommit).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("reword commit")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Reword commit")).
|
||||
InitialText(Equals("commit 02")).
|
||||
Clear().
|
||||
Type("renamed 02").
|
||||
|
||||
@@ -31,8 +31,8 @@ var RewordYouAreHereCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Commits.RenameCommit).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("reword commit")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Reword commit")).
|
||||
InitialText(Equals("commit 02")).
|
||||
Clear().
|
||||
Type("renamed 02").
|
||||
|
||||
@@ -39,11 +39,15 @@ var ApplyInReverseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
// Add both files to the patch; the first will conflict, the second won't
|
||||
PressPrimaryAction().
|
||||
Tap(func() {
|
||||
t.Views().Information().Content(Contains("building patch"))
|
||||
|
||||
t.Views().PatchBuildingSecondary().Content(
|
||||
Contains("+more file1 content"))
|
||||
}).
|
||||
SelectNextItem().
|
||||
PressPrimaryAction()
|
||||
|
||||
t.Views().Information().Content(Contains("building patch"))
|
||||
|
||||
t.Views().PatchBuildingSecondary().Content(
|
||||
Contains("+more file1 content").Contains("+more file2 content"))
|
||||
|
||||
|
||||
@@ -49,10 +49,13 @@ var tests = []*components.IntegrationTest{
|
||||
commit.CommitMultiline,
|
||||
commit.CreateTag,
|
||||
commit.DiscardOldFileChange,
|
||||
commit.History,
|
||||
commit.HistoryComplex,
|
||||
commit.NewBranch,
|
||||
commit.ResetAuthor,
|
||||
commit.Revert,
|
||||
commit.RevertMerge,
|
||||
commit.Reword,
|
||||
commit.Search,
|
||||
commit.SetAuthor,
|
||||
commit.StageRangeOfLines,
|
||||
@@ -64,13 +67,13 @@ var tests = []*components.IntegrationTest{
|
||||
conflicts.ResolveExternally,
|
||||
conflicts.ResolveMultipleFiles,
|
||||
conflicts.UndoChooseHunk,
|
||||
custom_commands.BasicCmdFromConfig,
|
||||
custom_commands.BasicCmdAtRuntime,
|
||||
custom_commands.OmitFromHistory,
|
||||
custom_commands.BasicCmdFromConfig,
|
||||
custom_commands.FormPrompts,
|
||||
custom_commands.MenuFromCommand,
|
||||
custom_commands.MenuFromCommandsOutput,
|
||||
custom_commands.MultiplePrompts,
|
||||
custom_commands.OmitFromHistory,
|
||||
diff.Diff,
|
||||
diff.DiffAndApplyPatch,
|
||||
diff.DiffCommits,
|
||||
@@ -83,10 +86,13 @@ var tests = []*components.IntegrationTest{
|
||||
filter_by_path.CliArg,
|
||||
filter_by_path.SelectFile,
|
||||
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.EditFirstCommit,
|
||||
interactive_rebase.EditNonTodoCommitDuringRebase,
|
||||
interactive_rebase.FixupFirstCommit,
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"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)
|
||||
}
|
||||
|
||||
func ReadRebaseTodoFile(fileName string) ([]todo.Todo, error) {
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
todos, err := todo.Parse(f)
|
||||
err2 := f.Close()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return todos, err
|
||||
}
|
||||
|
||||
func WriteRebaseTodoFile(fileName string, todos []todo.Todo) error {
|
||||
f, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = todo.Write(f, todos)
|
||||
err2 := f.Close()
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
rearrangedTodos, err := moveTodoDown(todos, sha, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteRebaseTodoFile(fileName, rearrangedTodos)
|
||||
}
|
||||
|
||||
func MoveTodoUp(fileName string, sha string, action todo.TodoCommand) error {
|
||||
todos, err := ReadRebaseTodoFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rearrangedTodos, err := moveTodoUp(todos, sha, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteRebaseTodoFile(fileName, rearrangedTodos)
|
||||
}
|
||||
|
||||
func moveTodoDown(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.Todo, error) {
|
||||
rearrangedTodos, err := moveTodoUp(lo.Reverse(todos), sha, action)
|
||||
return lo.Reverse(rearrangedTodos), err
|
||||
}
|
||||
|
||||
func moveTodoUp(todos []todo.Todo, sha string, action todo.TodoCommand) ([]todo.Todo, error) {
|
||||
_, sourceIdx, ok := lo.FindIndexOf(todos, func(t todo.Todo) bool {
|
||||
// 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)
|
||||
return t.Command == action && equalShas(t.Commit, sha)
|
||||
})
|
||||
|
||||
if !ok {
|
||||
// Should never happen
|
||||
return []todo.Todo{}, fmt.Errorf("Todo %s not found in git-rebase-todo", sha)
|
||||
}
|
||||
|
||||
// The todos are ordered backwards compared to our model commits, so
|
||||
// actually move the commit _down_ in the todos slice (i.e. towards
|
||||
// the end of the slice)
|
||||
|
||||
// Find the next todo that we show in lazygit's commits view (skipping the rest)
|
||||
_, skip, ok := lo.FindIndexOf(todos[sourceIdx+1:], isRenderedTodo)
|
||||
|
||||
if !ok {
|
||||
// We expect callers to guard against this
|
||||
return []todo.Todo{}, fmt.Errorf("Destination position for moving todo is out of range")
|
||||
}
|
||||
|
||||
destinationIdx := sourceIdx + 1 + skip
|
||||
|
||||
rearrangedTodos := MoveElement(todos, sourceIdx, destinationIdx)
|
||||
|
||||
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 {
|
||||
return t.Commit != "" || t.Command == todo.UpdateRef
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRebaseCommands_moveTodoDown(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
shaToMoveDown string
|
||||
expectedErr string
|
||||
expectedTodos []todo.Todo
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "simple case 1 - move to beginning",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "5678",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "simple case 2 - move from end",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "abcd",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "skip an invisible todo",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "def0"},
|
||||
},
|
||||
shaToMoveDown: "5678",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Pick, Commit: "def0"},
|
||||
},
|
||||
},
|
||||
|
||||
// Error cases
|
||||
{
|
||||
testName: "commit not found",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "def0",
|
||||
expectedErr: "Todo def0 not found in git-rebase-todo",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
{
|
||||
testName: "trying to move first commit down",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "1234",
|
||||
expectedErr: "Destination position for moving todo is out of range",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
{
|
||||
testName: "trying to move commit down when all commits before are invisible",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Reset, Label: "otherlabel"},
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
},
|
||||
shaToMoveDown: "1234",
|
||||
expectedErr: "Destination position for moving todo is out of range",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
rearrangedTodos, err := moveTodoDown(s.todos, s.shaToMoveDown, todo.Pick)
|
||||
if s.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, s.expectedErr)
|
||||
}
|
||||
assert.Equal(t, s.expectedTodos, rearrangedTodos)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebaseCommands_moveTodoUp(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
todos []todo.Todo
|
||||
shaToMoveDown string
|
||||
expectedErr string
|
||||
expectedTodos []todo.Todo
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "simple case 1 - move to end",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "5678",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "simple case 2 - move from beginning",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "1234",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "skip an invisible todo",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "def0"},
|
||||
},
|
||||
shaToMoveDown: "abcd",
|
||||
expectedErr: "",
|
||||
expectedTodos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
{Command: todo.Pick, Commit: "def0"},
|
||||
},
|
||||
},
|
||||
|
||||
// Error cases
|
||||
{
|
||||
testName: "commit not found",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "def0",
|
||||
expectedErr: "Todo def0 not found in git-rebase-todo",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
{
|
||||
testName: "trying to move last commit up",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Pick, Commit: "abcd"},
|
||||
},
|
||||
shaToMoveDown: "abcd",
|
||||
expectedErr: "Destination position for moving todo is out of range",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
{
|
||||
testName: "trying to move commit up when all commits after it are invisible",
|
||||
todos: []todo.Todo{
|
||||
{Command: todo.Pick, Commit: "1234"},
|
||||
{Command: todo.Pick, Commit: "5678"},
|
||||
{Command: todo.Label, Label: "myLabel"},
|
||||
{Command: todo.Reset, Label: "otherlabel"},
|
||||
},
|
||||
shaToMoveDown: "5678",
|
||||
expectedErr: "Destination position for moving todo is out of range",
|
||||
expectedTodos: []todo.Todo{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
rearrangedTodos, err := moveTodoUp(s.todos, s.shaToMoveDown, todo.Pick)
|
||||
if s.expectedErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.ErrorContains(t, err, s.expectedErr)
|
||||
}
|
||||
assert.Equal(t, s.expectedTodos, rearrangedTodos)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,3 +92,24 @@ func MuiltiGroupBy[T any, K comparable](slice []T, f func(T) []K) map[K][]T {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns a new slice with the element at index 'from' moved to index 'to'.
|
||||
// Does not mutate original slice.
|
||||
func MoveElement[T any](slice []T, from int, to int) []T {
|
||||
newSlice := make([]T, len(slice))
|
||||
copy(newSlice, slice)
|
||||
|
||||
if from == to {
|
||||
return newSlice
|
||||
}
|
||||
|
||||
if from < to {
|
||||
copy(newSlice[from:to+1], newSlice[from+1:to+1])
|
||||
} else {
|
||||
copy(newSlice[to+1:from+1], newSlice[to:from])
|
||||
}
|
||||
|
||||
newSlice[to] = slice[from]
|
||||
|
||||
return newSlice
|
||||
}
|
||||
|
||||
@@ -233,3 +233,85 @@ func TestLimitStr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveElement(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
list []int
|
||||
from int
|
||||
to int
|
||||
expected []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"no elements",
|
||||
[]int{},
|
||||
0,
|
||||
0,
|
||||
[]int{},
|
||||
},
|
||||
{
|
||||
"one element",
|
||||
[]int{1},
|
||||
0,
|
||||
0,
|
||||
[]int{1},
|
||||
},
|
||||
{
|
||||
"two elements, moving first to second",
|
||||
[]int{1, 2},
|
||||
0,
|
||||
1,
|
||||
[]int{2, 1},
|
||||
},
|
||||
{
|
||||
"two elements, moving second to first",
|
||||
[]int{1, 2},
|
||||
1,
|
||||
0,
|
||||
[]int{2, 1},
|
||||
},
|
||||
{
|
||||
"three elements, moving first to second",
|
||||
[]int{1, 2, 3},
|
||||
0,
|
||||
1,
|
||||
[]int{2, 1, 3},
|
||||
},
|
||||
{
|
||||
"three elements, moving second to first",
|
||||
[]int{1, 2, 3},
|
||||
1,
|
||||
0,
|
||||
[]int{2, 1, 3},
|
||||
},
|
||||
{
|
||||
"three elements, moving second to third",
|
||||
[]int{1, 2, 3},
|
||||
1,
|
||||
2,
|
||||
[]int{1, 3, 2},
|
||||
},
|
||||
{
|
||||
"three elements, moving third to second",
|
||||
[]int{1, 2, 3},
|
||||
2,
|
||||
1,
|
||||
[]int{1, 3, 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, MoveElement(s.list, s.from, s.to))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("from out of bounds", func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
MoveElement([]int{1, 2, 3}, 3, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Find all Go files in the project directory and its subdirectories, except in the vendor directory
|
||||
for file in $(find . -name "*.go" -not -path "./vendor/*"); do
|
||||
|
||||
# Check if the file name contains uppercase letters
|
||||
if [[ "$file" =~ [A-Z] ]]; then
|
||||
echo "Error: $file contains uppercase letters. All Go files in the project (excluding vendor directory) must use snake_case"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All Go files in the project (excluding vendor directory) use lowercase letters"
|
||||
exit 0
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2013 @atotto. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build plan9
|
||||
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"os"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func readAll() (string, error) {
|
||||
f, err := os.Open("/dev/snarf")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
str, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(str), nil
|
||||
}
|
||||
|
||||
func writeAll(text string) error {
|
||||
f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Write([]byte(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+26
-6
@@ -15,14 +15,17 @@ import (
|
||||
const (
|
||||
xsel = "xsel"
|
||||
xclip = "xclip"
|
||||
wlcopy = "wl-copy"
|
||||
wlpaste = "wl-paste"
|
||||
powershellExe = "powershell.exe"
|
||||
clipExe = "clip.exe"
|
||||
wlcopy = "wl-copy"
|
||||
wlpaste = "wl-paste"
|
||||
termuxClipboardGet = "termux-clipboard-get"
|
||||
termuxClipboardSet = "termux-clipboard-set"
|
||||
)
|
||||
|
||||
var (
|
||||
Primary bool
|
||||
trimDos bool
|
||||
|
||||
pasteCmdArgs []string
|
||||
copyCmdArgs []string
|
||||
@@ -33,8 +36,11 @@ var (
|
||||
xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"}
|
||||
xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"}
|
||||
|
||||
powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"}
|
||||
clipExeCopyArgs = []string{clipExe}
|
||||
|
||||
wlpasteArgs = []string{wlpaste, "--no-newline"}
|
||||
wlcopyArgs = []string{wlcopy}
|
||||
wlcopyArgs = []string{wlcopy}
|
||||
|
||||
termuxPasteArgs = []string{termuxClipboardGet}
|
||||
termuxCopyArgs = []string{termuxClipboardSet}
|
||||
@@ -44,8 +50,8 @@ var (
|
||||
|
||||
func init() {
|
||||
if os.Getenv("WAYLAND_DISPLAY") != "" {
|
||||
pasteCmdArgs = wlpasteArgs;
|
||||
copyCmdArgs = wlcopyArgs;
|
||||
pasteCmdArgs = wlpasteArgs
|
||||
copyCmdArgs = wlcopyArgs
|
||||
|
||||
if _, err := exec.LookPath(wlcopy); err == nil {
|
||||
if _, err := exec.LookPath(wlpaste); err == nil {
|
||||
@@ -77,6 +83,16 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
pasteCmdArgs = powershellExePasteArgs
|
||||
copyCmdArgs = clipExeCopyArgs
|
||||
trimDos = true
|
||||
|
||||
if _, err := exec.LookPath(clipExe); err == nil {
|
||||
if _, err := exec.LookPath(powershellExe); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Unsupported = true
|
||||
}
|
||||
|
||||
@@ -103,7 +119,11 @@ func readAll() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
result := string(out)
|
||||
if trimDos && len(result) > 1 {
|
||||
result = result[:len(result)-2]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeAll(text string) error {
|
||||
|
||||
+37
-8
@@ -7,6 +7,7 @@
|
||||
package clipboard
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -18,12 +19,13 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.MustLoadDLL("user32")
|
||||
openClipboard = user32.MustFindProc("OpenClipboard")
|
||||
closeClipboard = user32.MustFindProc("CloseClipboard")
|
||||
emptyClipboard = user32.MustFindProc("EmptyClipboard")
|
||||
getClipboardData = user32.MustFindProc("GetClipboardData")
|
||||
setClipboardData = user32.MustFindProc("SetClipboardData")
|
||||
user32 = syscall.MustLoadDLL("user32")
|
||||
isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable")
|
||||
openClipboard = user32.MustFindProc("OpenClipboard")
|
||||
closeClipboard = user32.MustFindProc("CloseClipboard")
|
||||
emptyClipboard = user32.MustFindProc("EmptyClipboard")
|
||||
getClipboardData = user32.MustFindProc("GetClipboardData")
|
||||
setClipboardData = user32.MustFindProc("SetClipboardData")
|
||||
|
||||
kernel32 = syscall.NewLazyDLL("kernel32")
|
||||
globalAlloc = kernel32.NewProc("GlobalAlloc")
|
||||
@@ -50,19 +52,27 @@ func waitOpenClipboard() error {
|
||||
}
|
||||
|
||||
func readAll() (string, error) {
|
||||
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
|
||||
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 {
|
||||
return "", err
|
||||
}
|
||||
err := waitOpenClipboard()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer closeClipboard.Call()
|
||||
|
||||
h, _, err := getClipboardData.Call(cfUnicodetext)
|
||||
if h == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return "", err
|
||||
}
|
||||
|
||||
l, _, err := globalLock.Call(h)
|
||||
if l == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -70,21 +80,31 @@ func readAll() (string, error) {
|
||||
|
||||
r, _, err := globalUnlock.Call(h)
|
||||
if r == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return "", err
|
||||
}
|
||||
|
||||
closed, _, err := closeClipboard.Call()
|
||||
if closed == 0 {
|
||||
return "", err
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func writeAll(text string) error {
|
||||
// LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution).
|
||||
// Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
err := waitOpenClipboard()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeClipboard.Call()
|
||||
|
||||
r, _, err := emptyClipboard.Call(0)
|
||||
if r == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -94,6 +114,7 @@ func writeAll(text string) error {
|
||||
// been allocated using the function with the GMEM_MOVEABLE flag."
|
||||
h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0]))))
|
||||
if h == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
@@ -104,25 +125,33 @@ func writeAll(text string) error {
|
||||
|
||||
l, _, err := globalLock.Call(h)
|
||||
if l == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
|
||||
r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0])))
|
||||
if r == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
|
||||
r, _, err = globalUnlock.Call(h)
|
||||
if r == 0 {
|
||||
if err.(syscall.Errno) != 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r, _, err = setClipboardData.Call(cfUnicodetext, h)
|
||||
if r == 0 {
|
||||
_, _, _ = closeClipboard.Call()
|
||||
return err
|
||||
}
|
||||
h = 0 // suppress deferred cleanup
|
||||
closed, _, err := closeClipboard.Call()
|
||||
if closed == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
Copyright © 2023 Flavio Miamoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+21
-4
@@ -13,6 +13,7 @@ var (
|
||||
ErrMissingLabel = errors.New("missing label")
|
||||
ErrMissingCommit = errors.New("missing commit")
|
||||
ErrMissingExecCmd = errors.New("missing command for exec")
|
||||
ErrMissingRef = errors.New("missing ref")
|
||||
)
|
||||
|
||||
func Parse(f io.Reader) ([]Todo, error) {
|
||||
@@ -55,9 +56,11 @@ func parseLine(line string) (Todo, error) {
|
||||
|
||||
fields := strings.Fields(line)
|
||||
|
||||
for i := TodoCommand(Pick); i < Comment; i++ {
|
||||
var commandLen int
|
||||
for i := Pick; i < Comment; i++ {
|
||||
if isCommand(i, fields[0]) {
|
||||
todo.Command = TodoCommand(i)
|
||||
todo.Command = i
|
||||
commandLen = len(fields[0])
|
||||
fields = fields[1:]
|
||||
break
|
||||
}
|
||||
@@ -73,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
|
||||
}
|
||||
|
||||
@@ -90,6 +97,7 @@ func parseLine(line string) (Todo, error) {
|
||||
|
||||
if todo.Command == Merge {
|
||||
if fields[0] == "-C" || fields[0] == "-c" {
|
||||
todo.Flag = fields[0]
|
||||
fields = fields[1:]
|
||||
if len(fields) == 0 {
|
||||
return todo, ErrMissingCommit
|
||||
@@ -115,10 +123,19 @@ func parseLine(line string) (Todo, error) {
|
||||
}
|
||||
// Skip flags
|
||||
if fields[0] == "-C" || fields[0] == "-c" {
|
||||
todo.Flag = fields[0]
|
||||
fields = fields[1:]
|
||||
}
|
||||
}
|
||||
|
||||
if todo.Command == UpdateRef {
|
||||
if len(fields) == 0 {
|
||||
return todo, ErrMissingRef
|
||||
}
|
||||
todo.Ref = fields[0]
|
||||
return todo, nil
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return todo, ErrMissingCommit
|
||||
}
|
||||
|
||||
+20
-15
@@ -18,6 +18,7 @@ const (
|
||||
|
||||
NoOp
|
||||
Drop
|
||||
UpdateRef
|
||||
|
||||
Comment
|
||||
)
|
||||
@@ -27,10 +28,12 @@ const CommentChar = "#"
|
||||
type Todo struct {
|
||||
Command TodoCommand
|
||||
Commit string
|
||||
Flag string
|
||||
Comment string
|
||||
ExecCommand string
|
||||
Label string
|
||||
Msg string
|
||||
Ref string
|
||||
}
|
||||
|
||||
func (t TodoCommand) String() string {
|
||||
@@ -38,23 +41,24 @@ func (t TodoCommand) String() string {
|
||||
}
|
||||
|
||||
var commandToString = map[TodoCommand]string{
|
||||
Pick: "pick",
|
||||
Revert: "revert",
|
||||
Edit: "edit",
|
||||
Reword: "reword",
|
||||
Fixup: "fixup",
|
||||
Squash: "squash",
|
||||
Exec: "exec",
|
||||
Break: "break",
|
||||
Label: "label",
|
||||
Reset: "reset",
|
||||
Merge: "merge",
|
||||
NoOp: "noop",
|
||||
Drop: "drop",
|
||||
Comment: "comment",
|
||||
Pick: "pick",
|
||||
Revert: "revert",
|
||||
Edit: "edit",
|
||||
Reword: "reword",
|
||||
Fixup: "fixup",
|
||||
Squash: "squash",
|
||||
Exec: "exec",
|
||||
Break: "break",
|
||||
Label: "label",
|
||||
Reset: "reset",
|
||||
Merge: "merge",
|
||||
NoOp: "noop",
|
||||
Drop: "drop",
|
||||
UpdateRef: "update-ref",
|
||||
Comment: "comment",
|
||||
}
|
||||
|
||||
var todoCommandInfo = [14]struct {
|
||||
var todoCommandInfo = [15]struct {
|
||||
nickname string
|
||||
cmd string
|
||||
}{
|
||||
@@ -72,4 +76,5 @@ var todoCommandInfo = [14]struct {
|
||||
{"m", "merge"},
|
||||
{"", "noop"},
|
||||
{"d", "drop"},
|
||||
{"u", "update-ref"},
|
||||
}
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package todo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Write(f io.Writer, todos []Todo) error {
|
||||
for _, todo := range todos {
|
||||
if err := writeTodo(f, todo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeTodo(f io.Writer, todo Todo) error {
|
||||
var sb strings.Builder
|
||||
if todo.Command != Comment {
|
||||
sb.WriteString(todo.Command.String())
|
||||
}
|
||||
|
||||
switch todo.Command {
|
||||
case NoOp:
|
||||
return nil
|
||||
|
||||
case Comment:
|
||||
sb.WriteString(CommentChar)
|
||||
sb.WriteString(todo.Comment)
|
||||
|
||||
case Break:
|
||||
|
||||
case Label:
|
||||
fallthrough
|
||||
case Reset:
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.Label)
|
||||
|
||||
case Exec:
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.ExecCommand)
|
||||
|
||||
case Merge:
|
||||
sb.WriteByte(' ')
|
||||
if todo.Commit != "" {
|
||||
sb.WriteString(todo.Flag)
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.Commit)
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
sb.WriteString(todo.Label)
|
||||
if todo.Msg != "" {
|
||||
sb.WriteString(" # ")
|
||||
sb.WriteString(todo.Msg)
|
||||
}
|
||||
|
||||
case Fixup:
|
||||
sb.WriteByte(' ')
|
||||
if todo.Flag != "" {
|
||||
sb.WriteString(todo.Flag)
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
sb.WriteString(todo.Commit)
|
||||
|
||||
case UpdateRef:
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.Ref)
|
||||
|
||||
case Pick:
|
||||
fallthrough
|
||||
case Revert:
|
||||
fallthrough
|
||||
case Edit:
|
||||
fallthrough
|
||||
case Reword:
|
||||
fallthrough
|
||||
case Squash:
|
||||
fallthrough
|
||||
case Drop:
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.Commit)
|
||||
if todo.Msg != "" {
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(todo.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteByte('\n')
|
||||
_, err := f.Write([]byte(sb.String()))
|
||||
return err
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user