Compare commits

..
Author SHA1 Message Date
Jesse Duffield e5e3cc200e fix test names 2023-04-14 20:24:35 +10:00
Jesse Duffield 8e4efddbf7 ensure test list is correct in CI 2023-04-14 20:21:55 +10:00
102 changed files with 729 additions and 3157 deletions
+3 -6
View File
@@ -135,13 +135,10 @@ 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
- name: Check Integration Tests List
# ensure our tests list matches up with our generator's output
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
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Unexpected change to tests list. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
lint:
runs-on: ubuntu-latest
env:
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -57,7 +57,6 @@ gui:
showFileTree: true # for rendering changes files in a tree format
showListFooter: true # for seeing the '5 of 20' message in list panels
showRandomTip: true
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
showCommandLog: true
showIcons: false
@@ -172,6 +171,7 @@ keybinding:
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<a-enter>'
extrasMenu: '@'
toggleWhitespaceInDiffView: '<c-w>'
increaseContextInDiffView: '}'
@@ -474,7 +474,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`, `gitlab` or `gitea`
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
+2 -2
View File
@@ -4,12 +4,12 @@ go 1.18
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/atotto/clipboard v0.1.2
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.4
github.com/fsmiamoto/git-todo-parser v0.0.2
github.com/fsnotify/fsnotify v1.4.7
github.com/gdamore/tcell/v2 v2.6.0
github.com/go-errors/errors v1.4.2
+2 -4
View File
@@ -8,8 +8,6 @@ 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=
@@ -30,8 +28,8 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
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/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=
+49 -249
View File
@@ -1,17 +1,13 @@
package daemon
import (
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"path/filepath"
"strings"
"github.com/fsmiamoto/git-todo-parser/todo"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/jesseduffield/lazygit/pkg/env"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
@@ -19,58 +15,33 @@ 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=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.
// '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.
type DaemonKind int
type DaemonKind string
const (
// for when we fail to parse the daemon kind
DaemonKindUnknown DaemonKind = iota
DaemonKindExitImmediately
DaemonKindCherryPick
DaemonKindMoveTodoUp
DaemonKindMoveTodoDown
DaemonKindInsertBreak
DaemonKindChangeTodoActions
DaemonKindMoveFixupCommitDown
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
)
const (
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
// Contains json-encoded arguments to the daemon
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
)
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)
type Daemon interface {
Run() error
}
func Handle(common *common.Common) {
if !InDaemonMode() {
d := getDaemon(common)
if d == nil {
return
}
instruction := getInstruction()
if err := instruction.run(common); err != nil {
if err := d.Run(); err != nil {
log.Fatal(err)
}
@@ -78,229 +49,58 @@ func Handle(common *common.Common) {
}
func InDaemonMode() bool {
return getDaemonKind() != DaemonKindUnknown
return getDaemonKind() != ""
}
func getDaemonKind() DaemonKind {
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
if err != nil {
return DaemonKindUnknown
func getDaemon(common *common.Common) Daemon {
switch getDaemonKind() {
case InteractiveRebase:
return &rebaseDaemon{c: common}
case ExitImmediately:
return &exitImmediatelyDaemon{c: common}
}
return DaemonKind(intValue)
}
// An Instruction is a command to be run by lazygit in daemon mode.
// It is serialized to json and passed to lazygit via environment variables
type Instruction interface {
Kind() DaemonKind
SerializedInstructions() string
// runs the instruction
run(common *common.Common) error
}
func serializeInstruction[T any](instruction T) string {
jsonData, err := json.Marshal(instruction)
if err != nil {
// this should never happen
panic(err)
}
return string(jsonData)
}
func deserializeInstruction[T Instruction](jsonData string) Instruction {
var instruction T
err := json.Unmarshal([]byte(jsonData), &instruction)
if err != nil {
panic(err)
}
return instruction
}
func ToEnvVars(instruction Instruction) []string {
return []string{
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
}
}
type ExitImmediatelyInstruction struct{}
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
return DaemonKindExitImmediately
}
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
return nil
}
func NewExitImmediatelyInstruction() Instruction {
return &ExitImmediatelyInstruction{}
func getDaemonKind() DaemonKind {
return DaemonKind(os.Getenv(DaemonKindEnvKey))
}
type CherryPickCommitsInstruction struct {
Todo string
type rebaseDaemon struct {
c *common.Common
}
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
return TodoLine{
Action: "pick",
Commit: commit,
func (self *rebaseDaemon) Run() error {
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
self.c.Log.Info("args: ", os.Args)
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
}
})
todo := TodoLinesToString(todoLines)
return &CherryPickCommitsInstruction{
Todo: todo,
} 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")
}
return nil
}
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
return DaemonKindCherryPick
}
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
})
}
type ChangeTodoActionsInstruction struct {
Changes []ChangeTodoAction
}
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
return &ChangeTodoActionsInstruction{
Changes: changes,
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
return DaemonKindChangeTodoActions
type exitImmediatelyDaemon struct {
c *common.Common
}
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"))
})
func (self *exitImmediatelyDaemon) Run() error {
return nil
}
-64
View File
@@ -1,64 +0,0 @@
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
}
+27 -28
View File
@@ -87,34 +87,33 @@ 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,
"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,
"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,
}
title, ok := contextTitleMap[str]
+11 -30
View File
@@ -8,8 +8,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
var ErrInvalidCommitIndex = errors.New("invalid commit index")
type CommitCommands struct {
*GitCommon
}
@@ -20,6 +18,11 @@ 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()
@@ -42,7 +45,11 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
}
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(message)
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
}
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
noVerifyFlag := ""
@@ -50,23 +57,7 @@ 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(), 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)
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
}
// runs git commit without the -m argument meaning it will invoke the user's editor
@@ -187,13 +178,3 @@ 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)
}
+16 -24
View File
@@ -89,7 +89,7 @@ func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit,
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
return false, nil
})
@@ -194,8 +194,8 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
return nil, nil
}
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
return commit.Sha, commit.Sha != ""
commitShas := slices.Map(commits, func(commit *models.Commit) string {
return commit.Sha
})
// note that we're not filtering these as we do non-rebasing commits just because
@@ -209,26 +209,20 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
),
).DontLog()
fullCommits := map[string]*models.Commit{}
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
commit := self.extractCommitFromLine(line)
fullCommits[commit.Sha] = commit
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
hydratedCommits = append(hydratedCommits, commit)
i++
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
}
@@ -311,17 +305,15 @@ func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, err
}
for _, t := range todos {
if t.Command == todo.UpdateRef {
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
} else if t.Commit == "" {
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: models.StatusRebasing,
Action: t.Command,
Status: "rebasing",
Action: t.Command.String(),
})
}
@@ -340,7 +332,7 @@ func (self *CommitLoader) commitFromPatch(content string) *models.Commit {
return &models.Commit{
Sha: sha,
Name: name,
Status: models.StatusRebasing,
Status: "rebasing",
}
}
@@ -357,11 +349,11 @@ func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*mod
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != models.StatusPushed {
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = models.StatusMerged
commits[i].Status = "merged"
}
}
return commits, nil
+16 -16
View File
@@ -78,8 +78,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: models.StatusUnpushed,
Action: models.ActionNone,
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
AuthorName: "Jesse Duffield",
@@ -92,8 +92,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
AuthorName: "Jesse Duffield",
@@ -106,8 +106,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -120,8 +120,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -134,8 +134,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: models.StatusPushed,
Action: models.ActionNone,
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -148,8 +148,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -162,8 +162,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
@@ -176,8 +176,8 @@ func TestGetCommits(t *testing.T) {
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: models.StatusMerged,
Action: models.ActionNone,
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
AuthorName: "Jesse Duffield",
+5 -62
View File
@@ -9,32 +9,12 @@ import (
)
func TestCommitRewordCommit(t *testing.T) {
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})
runner := oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
instance := buildCommitCommands(commonDeps{runner: runner})
assert.NoError(t, instance.RewordLastCommit(s.input))
s.runner.CheckForMissingCalls()
})
}
assert.NoError(t, instance.RewordLastCommit("test"))
runner.CheckForMissingCalls()
}
func TestCommitResetToCommit(t *testing.T) {
@@ -294,40 +274,3 @@ 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)
})
}
}
+8 -12
View File
@@ -3,9 +3,7 @@ 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"
@@ -105,17 +103,15 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
baseIndex := sourceCommitIdx + 1
changes := []daemon.ChangeTodoAction{
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
}
self.os.LogCommand(logTodoChanges(changes), false)
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == sourceCommitIdx || i == destinationCommitIdx {
return "edit"
} else {
return "pick"
}
})
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: commits[baseIndex].Sha,
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
if err != nil {
return err
}
+146 -125
View File
@@ -2,17 +2,15 @@ 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 {
@@ -56,16 +54,12 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
}
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
changes := []daemon.ChangeTodoAction{{
Sha: commits[index].Sha,
NewAction: todo.Reword,
}}
self.os.LogCommand(logTodoChanges(changes), false)
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}), nil
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
}
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
@@ -81,7 +75,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
}
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
if models.IsHeadCommit(commits, index) {
if index == 0 {
// we've selected the top commit so no rebase is required
return f()
}
@@ -101,79 +95,41 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
}
func (self *RebaseCommands) MoveCommitDown(commits []*models.Commit, index int) error {
// not appending to original slice so that we don't mutate it
orderedCommits := append([]*models.Commit{}, commits[0:index]...)
orderedCommits = append(orderedCommits, commits[index+1], commits[index])
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoDownInstruction(sha),
overrideEditor: true,
}).Run()
return self.PrepareInteractiveRebaseCommand(baseShaOrRoot, todoLines, true).Run()
}
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
sha := commits[index].Sha
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: baseShaOrRoot,
instruction: daemon.NewMoveTodoUpInstruction(sha),
overrideEditor: true,
}).Run()
}
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
baseIndex := index + 1
if action == todo.Squash || action == todo.Fixup {
baseIndex++
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
}
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()
return self.PrepareInteractiveRebaseCommand(sha, todo, true).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 (self *RebaseCommands) InteractiveRebaseBreakAfter(commits []*models.Commit, index int) error {
todo, sha, err := self.BuildSingleActionTodo(commits, index-1, "pick")
if err != nil {
return err
}
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
todo = append(todo, TodoLine{Action: "break", Commit: nil})
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseShaOrRoot string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
todo := self.buildTodo(todoLines)
ex := oscommands.GetLazygitPath()
debug := "FALSE"
@@ -181,75 +137,119 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteract
debug = "TRUE"
}
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)
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash %s", baseShaOrRoot)
self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr)
gitSequenceEditor := ex
if opts.instruction != nil {
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
} else {
if todo == "" {
gitSequenceEditor = "true"
} else {
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
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 opts.overrideEditor {
if overrideEditor {
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmdObj
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
commit := commits[commitIndex]
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
// will have the given action applied to it.
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
baseIndex := actionIndex + 1
if action == "squash" || action == "fixup" {
baseIndex++
}
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
if i == actionIndex {
return action
} else if commit.IsMerge() {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
return "drop"
} else {
return "pick"
}
})
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
return todoLines, baseShaOrRoot, nil
}
// AmendTo amends the given commit with whatever files are staged
func (self *RebaseCommands) AmendTo(commit *models.Commit) error {
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
return err
}
// Get the sha of the commit we just created
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
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)
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
}).Run()
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)
}
// 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)
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
}
// MoveTodoDown moves a rebase todo item down by one position
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
func (self *RebaseCommands) MoveTodoDown(index int) error {
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
}
bytes, err := os.ReadFile(fileName)
if err != nil {
return err
}
// 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)
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)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
@@ -283,22 +283,17 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
return errors.New(self.Tr.DisabledForGPG)
}
changes := []daemon.ChangeTodoAction{{
Sha: commits[commitIndex].Sha,
NewAction: todo.Edit,
}}
self.os.LogCommand(logTodoChanges(changes), false)
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
overrideEditor: true,
instruction: daemon.NewChangeTodoActionsInstruction(changes),
}).Run()
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
}
// RebaseBranch interactive rebases onto a branch
func (self *RebaseCommands) RebaseBranch(branchName string) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
}
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
@@ -339,16 +334,15 @@ 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()
}
@@ -382,15 +376,42 @@ 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 {
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)
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseShaOrRoot: "HEAD",
instruction: daemon.NewCherryPickCommitsInstruction(commits),
}).Run()
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()
})
return strings.Join(slices.Reverse(lines), "")
}
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
return TodoLine{Action: f(commit, i), Commit: commit}
})
}
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
return action
})
}
type TodoLine struct {
Action string
Commit *models.Commit
}
func (self *TodoLine) ToString() string {
if self.Action == "break" {
return self.Action + "\n"
} else {
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
}
}
// we can't start an interactive rebase from the first commit without passing the
+1 -2
View File
@@ -2,7 +2,6 @@ package git_commands
import (
"regexp"
"strconv"
"testing"
"github.com/go-errors/errors"
@@ -64,7 +63,7 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
`^GIT_SEQUENCE_EDITOR=.*$`,
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
} {
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: models.StatusReflog,
Status: "reflog",
Parents: parents,
}
@@ -51,35 +51,35 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from B to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from master to A",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
{
Sha: "f4ddf2f0d4be4ccc7efa",
Name: "checkout: moving from A to master",
Status: models.StatusReflog,
Status: "reflog",
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: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -103,7 +103,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
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: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
@@ -128,7 +128,7 @@ func TestGetReflogCommits(t *testing.T) {
{
Sha: "c3c4b66b64c97ffeecde",
Name: "checkout: moving from A to B",
Status: models.StatusReflog,
Status: "reflog",
UnixTimestamp: 1643150483,
Parents: []string{"51baa8c1"},
},
+5 -1
View File
@@ -29,7 +29,11 @@ func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error)
cmdStr := "git push"
if opts.Force {
cmdStr += " --force-with-lease"
if self.version.IsOlderThan(2, 30, 0) {
cmdStr += " --force-with-lease"
} else {
cmdStr += " --force-with-lease --force-if-includes"
}
}
if opts.SetUpstream {
+31 -1
View File
@@ -10,6 +10,7 @@ import (
func TestSyncPush(t *testing.T) {
type scenario struct {
testName string
version *GitVersion
opts PushOpts
test func(oscommands.ICmdObj, error)
}
@@ -17,6 +18,7 @@ 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")
@@ -25,14 +27,25 @@ 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",
@@ -45,6 +58,7 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force disabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
@@ -58,6 +72,7 @@ func TestSyncPush(t *testing.T) {
},
{
testName: "Push with force enabled, setting upstream",
version: &GitVersion{2, 29, 3, ""},
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
@@ -69,8 +84,23 @@ 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: "",
@@ -87,7 +117,7 @@ func TestSyncPush(t *testing.T) {
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildSyncCommands(commonDeps{})
instance := buildSyncCommands(commonDeps{gitVersion: s.version})
s.test(instance.PushCmdObj(s.opts))
})
}
+1 -1
View File
@@ -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 tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
}
+1 -5
View File
@@ -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,7 +65,3 @@ 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,22 +64,12 @@ 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{
@@ -103,9 +93,4 @@ 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, gitea, etc.
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, 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,60 +264,6 @@ 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",
@@ -364,7 +310,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, gitea"},
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
},
{
testName: "Escapes reserved URL characters in from branch name",
+3 -22
View File
@@ -3,37 +3,18 @@ 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 CommitStatus
Action todo.TodoCommand
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
AuthorName string // something like 'Jesse Duffield'
@@ -82,7 +63,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 != ActionNone
return c.Action != ""
}
func IsHeadCommit(commits []*Commit, index int) bool {
+5 -7
View File
@@ -37,12 +37,11 @@ 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"),
"kakoune": standardTerminalEditorPreset("kakoune"),
"vi": standardTerminalEditorPreset("vi"),
"vim": standardTerminalEditorPreset("vim"),
"nvim": standardTerminalEditorPreset("nvim"),
"emacs": standardTerminalEditorPreset("emacs"),
"nano": standardTerminalEditorPreset("nano"),
"vscode": {
editTemplate: "code --reuse-window -- {{filename}}",
editAtLineTemplate: "code --reuse-window --goto -- {{filename}}:{{line}}",
@@ -71,7 +70,6 @@ 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",
+40 -42
View File
@@ -27,33 +27,32 @@ type RefresherConfig struct {
}
type GuiConfig struct {
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
ExperimentalShowBranchHeads bool `yaml:"experimentalShowBranchHeads"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
AuthorColors map[string]string `yaml:"authorColors"`
BranchColors map[string]string `yaml:"branchColors"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Language string `yaml:"language"`
TimeFormat string `yaml:"timeFormat"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
ShowListFooter bool `yaml:"showListFooter"`
ShowFileTree bool `yaml:"showFileTree"`
ShowRandomTip bool `yaml:"showRandomTip"`
ShowCommandLog bool `yaml:"showCommandLog"`
ShowBottomLine bool `yaml:"showBottomLine"`
ShowIcons bool `yaml:"showIcons"`
CommandLogSize int `yaml:"commandLogSize"`
SplitDiff string `yaml:"splitDiff"`
SkipRewordInEditorWarning bool `yaml:"skipRewordInEditorWarning"`
WindowSize string `yaml:"windowSize"`
Border string `yaml:"border"`
}
type ThemeConfig struct {
@@ -166,7 +165,6 @@ 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"`
@@ -195,6 +193,7 @@ 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"`
@@ -409,19 +408,18 @@ func GetDefaultConfig() *UserConfig {
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
ExperimentalShowBranchHeads: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
ShowListFooter: true,
ShowCommandLog: true,
ShowBottomLine: true,
ShowFileTree: true,
ShowRandomTip: true,
ShowIcons: false,
CommandLogSize: 8,
SplitDiff: "auto",
SkipRewordInEditorWarning: false,
Border: "single",
},
Git: GitConfig{
Paging: PagingConfig{
@@ -494,7 +492,6 @@ func GetDefaultConfig() *UserConfig {
Select: "<space>",
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmInEditor: "<a-enter>",
Remove: "d",
New: "n",
Edit: "e",
@@ -523,6 +520,7 @@ func GetDefaultConfig() *UserConfig {
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<a-enter>",
ExtrasMenu: "@",
ToggleWhitespaceInDiffView: "<c-w>",
IncreaseContextInDiffView: "}",
+1
View File
@@ -15,6 +15,7 @@ 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),
},
)
-8
View File
@@ -1,7 +1,6 @@
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"
@@ -35,13 +34,6 @@ 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)
+10 -4
View File
@@ -78,11 +78,17 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
return lineCount
}
func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight)
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) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) {
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) {
width, height := gui.g.Size()
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
@@ -180,7 +186,7 @@ func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPane
}
confirmationView := gui.Views.Confirmation
confirmationView.Editable = opts.Editable
confirmationView.Editor = gocui.EditorFunc(gui.promptEditor)
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.Editable {
textArea := confirmationView.TextArea
-31
View File
@@ -9,7 +9,6 @@ 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
@@ -147,36 +146,6 @@ 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())
-95
View File
@@ -1,95 +0,0 @@
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
}
+8 -11
View File
@@ -33,14 +33,13 @@ 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"
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"
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"
)
var AllContextKeys = []types.ContextKey{
@@ -98,8 +97,7 @@ type ContextTree struct {
CustomPatchBuilderSecondary types.Context
MergeConflicts *MergeConflictsContext
Confirmation types.Context
CommitMessage *CommitMessageContext
CommitDescription types.Context
CommitMessage types.Context
CommandLog types.Context
// display contexts
@@ -131,7 +129,6 @@ func (self *ContextTree) Flatten() []types.Context {
self.Menu,
self.Confirmation,
self.CommitMessage,
self.CommitDescription,
self.MergeConflicts,
self.StagingSecondary,
+5 -17
View File
@@ -217,29 +217,17 @@ func (gui *Gui) contextTree() *context.ContextTree {
},
},
),
CommitMessage: context.NewCommitMessageContext(
gui.Views.CommitMessage,
context.ContextCallbackOpts{
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
CommitDescription: context.NewSimpleContext(
CommitMessage: context.NewSimpleContext(
context.NewBaseContext(context.NewBaseContextOpts{
Kind: types.PERSISTENT_POPUP,
View: gui.Views.CommitDescription,
WindowName: "commitDescription",
Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY,
View: gui.Views.CommitMessage,
WindowName: "commitMessage",
Key: context.COMMIT_MESSAGE_CONTEXT_KEY,
Focusable: true,
HasUncontrolledBounds: true,
}),
context.ContextCallbackOpts{
OnFocus: func(opts types.OnFocusOpts) error {
_, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10)
if err != nil {
return err
}
return nil
},
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
},
),
Search: context.NewSimpleContext(
+26 -28
View File
@@ -9,6 +9,7 @@ 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"
)
@@ -25,24 +26,10 @@ func (gui *Gui) resetControllers() {
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper)
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions)
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())
setCommitMessage := gui.getSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
getSavedCommitMessage := func() string {
return gui.State.savedCommitMessage
}
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,
@@ -51,7 +38,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, setCommitSummary, commitsHelper, gpgHelper),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitMessage, getSavedCommitMessage),
Tags: helpers.NewTagsHelper(helperCommon, gui.git),
GPG: gpgHelper,
MergeAndRebase: rebaseHelper,
@@ -65,7 +52,6 @@ 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(
@@ -98,12 +84,27 @@ 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,
)
commitDescriptionController := controllers.NewCommitDescriptionController(
common,
getCommitMessage,
onCommitAttempt,
onCommitSuccess,
)
remoteBranchesController := controllers.NewRemoteBranchesController(common)
@@ -114,7 +115,8 @@ func (gui *Gui) resetControllers() {
filesController := controllers.NewFilesController(
common,
gui.enterSubmodule,
setCommitSummary,
setCommitMessage,
getSavedCommitMessage,
)
mergeConflictsController := controllers.NewMergeConflictsController(common)
remotesController := controllers.NewRemotesController(
@@ -248,10 +250,6 @@ func (gui *Gui) resetControllers() {
commitMessageController,
)
controllers.AttachControllers(gui.State.Contexts.CommitDescription,
commitDescriptionController,
)
controllers.AttachControllers(gui.State.Contexts.RemoteBranches,
remoteBranchesController,
)
@@ -1,60 +0,0 @@
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,24 +1,33 @@
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,
}
}
@@ -32,18 +41,6 @@ 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
@@ -53,63 +50,30 @@ func (self *CommitMessageController) Context() types.Context {
return self.context()
}
func (self *CommitMessageController) context() *context.CommitMessageContext {
// 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 {
return self.contexts.CommitMessage
}
func (self *CommitMessageController) handlePreviousCommit() error {
return self.handleCommitIndexChange(1)
}
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())
}
validCommit, err := self.setCommitMessageAtIndex(newIndex)
if validCommit {
self.context().SetSelectedIndex(newIndex)
}
return err
}
// 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()
message := self.getCommitMessage()
self.onCommitAttempt(message)
if message == "" {
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
}
cmdObj := self.git.Commit.CommitCmdObj(message)
self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.c.PopContext()
return self.helpers.GPG.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.onCommitSuccess()
return nil
})
}
func (self *CommitMessageController) close() error {
return self.helpers.Commits.CloseCommitMessagePanel()
return self.c.PopContext()
}
+8 -5
View File
@@ -15,8 +15,9 @@ type FilesController struct {
baseController // nolint: unused
*controllerCommon
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
enterSubmodule func(submodule *models.SubmoduleConfig) error
setCommitMessage func(message string)
getSavedCommitMessage func() string
}
var _ types.IController = &FilesController{}
@@ -25,11 +26,13 @@ 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,
controllerCommon: common,
enterSubmodule: enterSubmodule,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
}
}
@@ -1,166 +0,0 @@
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,
}
}
-2
View File
@@ -15,7 +15,6 @@ type Helpers struct {
GPG *GpgHelper
Upstream *UpstreamHelper
AmendHelper *AmendHelper
Commits *CommitsHelper
}
func NewStubHelpers() *Helpers {
@@ -34,6 +33,5 @@ func NewStubHelpers() *Helpers {
GPG: &GpgHelper{},
Upstream: &UpstreamHelper{},
AmendHelper: &AmendHelper{},
Commits: &CommitsHelper{},
}
}
@@ -201,42 +201,22 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
if ref == checkedOutBranch {
return self.c.ErrorMsg(self.c.Tr.CantRebaseOntoSelf)
}
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,
prompt := utils.ResolvePlaceholderString(
self.c.Tr.ConfirmRebase,
map[string]string{
"checkedOutBranch": checkedOutBranch,
"ref": ref,
"selectedBranch": ref,
},
)
return self.c.Menu(types.CreateMenuOptions{
Title: title,
Items: menuItems,
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)
},
})
}
@@ -20,14 +20,13 @@ type IWorkingTreeHelper interface {
}
type WorkingTreeHelper struct {
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
commitsHelper *CommitsHelper
gpgHelper *GpgHelper
c *types.HelperCommon
git *commands.GitCommand
contexts *context.ContextTree
refHelper *RefsHelper
model *types.Model
setCommitMessage func(message string)
getSavedCommitMessage func() string
}
func NewWorkingTreeHelper(
@@ -37,18 +36,16 @@ func NewWorkingTreeHelper(
refHelper *RefsHelper,
model *types.Model,
setCommitMessage func(message string),
commitsHelper *CommitsHelper,
gpgHelper *GpgHelper,
getSavedCommitMessage func() string,
) *WorkingTreeHelper {
return &WorkingTreeHelper{
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
commitsHelper: commitsHelper,
gpgHelper: gpgHelper,
c: c,
git: git,
contexts: contexts,
refHelper: refHelper,
model: model,
setCommitMessage: setCommitMessage,
getSavedCommitMessage: getSavedCommitMessage,
}
}
@@ -97,7 +94,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error {
})
}
func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage string) error {
func (self *WorkingTreeHelper) HandleCommitPress() error {
if err := self.prepareFilesForCommit(); err != nil {
return self.c.Error(err)
}
@@ -110,25 +107,28 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
return self.PromptToStageAllAndRetry(self.HandleCommitPress)
}
return self.commitsHelper.OpenCommitMessagePanel(
&OpenCommitMessagePanelOpts{
CommitIndex: context.NoCommitIndex,
InitialMessage: initialMessage,
Title: self.c.Tr.CommitSummary,
PreserveMessage: true,
OnConfirm: self.handleCommit,
},
)
}
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)
}
}
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
})
if err := self.c.PushContext(self.contexts.CommitMessage); err != nil {
return err
}
return nil
}
// HandleCommitEditorPress - handle when the user wants to commit changes via
@@ -154,27 +154,9 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error {
return self.c.ErrorMsg(self.c.Tr.SkipHookPrefixNotConfigured)
}
return self.HandleCommitPressWithMessage(skipHookPrefix)
}
self.setCommitMessage(skipHookPrefix)
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)
return self.HandleCommitPress()
}
func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) error {
+37 -64
View File
@@ -3,14 +3,11 @@ 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 (
@@ -20,6 +17,7 @@ type (
type LocalCommitsController struct {
baseController
*controllerCommon
pullFiles PullFilesFn
}
@@ -155,7 +153,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
}
applied, err := self.handleMidRebaseCommand(todo.Squash, commit)
applied, err := self.handleMidRebaseCommand("squash", commit)
if err != nil {
return err
}
@@ -169,7 +167,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(todo.Squash)
return self.interactiveRebase("squash")
})
},
})
@@ -180,7 +178,7 @@ func (self *LocalCommitsController) fixup(commit *models.Commit) error {
return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit)
}
applied, err := self.handleMidRebaseCommand(todo.Fixup, commit)
applied, err := self.handleMidRebaseCommand("fixup", commit)
if err != nil {
return err
}
@@ -194,14 +192,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(todo.Fixup)
return self.interactiveRebase("fixup")
})
},
})
}
func (self *LocalCommitsController) reword(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Reword, commit)
applied, err := self.handleMidRebaseCommand("reword", commit)
if err != nil {
return err
}
@@ -209,30 +207,24 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
return nil
}
commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha)
message, err := self.git.Commit.GetCommitMessage(commit.Sha)
if err != nil {
return self.c.Error(err)
}
return self.helpers.Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(),
InitialMessage: commitMessage,
Title: self.c.Tr.Actions.RewordCommit,
PreserveMessage: false,
OnConfirm: self.handleReword,
// 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})
},
)
}
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 {
@@ -256,7 +248,7 @@ func (self *LocalCommitsController) doRewordEditor() error {
}
func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
midRebase, err := self.handleMidRebaseCommand(todo.Reword, commit)
midRebase, err := self.handleMidRebaseCommand("reword", commit)
if err != nil {
return err
}
@@ -276,7 +268,7 @@ func (self *LocalCommitsController) rewordEditor(commit *models.Commit) error {
}
func (self *LocalCommitsController) drop(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Drop, commit)
applied, err := self.handleMidRebaseCommand("drop", commit)
if err != nil {
return err
}
@@ -290,14 +282,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(todo.Drop)
return self.interactiveRebase("drop")
})
},
})
}
func (self *LocalCommitsController) edit(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Edit, commit)
applied, err := self.handleMidRebaseCommand("edit", commit)
if err != nil {
return err
}
@@ -307,13 +299,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.EditRebase(commit.Sha)
err := self.git.Rebase.InteractiveRebaseBreakAfter(self.model.Commits, self.context().GetSelectedLineIdx())
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
})
}
func (self *LocalCommitsController) pick(commit *models.Commit) error {
applied, err := self.handleMidRebaseCommand(todo.Pick, commit)
applied, err := self.handleMidRebaseCommand("pick", commit)
if err != nil {
return err
}
@@ -326,7 +318,7 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error {
return self.pullFiles()
}
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) error {
func (self *LocalCommitsController) interactiveRebase(action string) error {
err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
}
@@ -334,12 +326,12 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand) e
// 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 todo.TodoCommand, commit *models.Commit) (bool, error) {
func (self *LocalCommitsController) handleMidRebaseCommand(action string, 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 == todo.Reword && self.isHeadCommit()) {
if !(action == "reword" && self.isHeadCommit()) {
return true, self.c.ErrorMsg(self.c.Tr.AlreadyRebasing)
}
}
@@ -351,21 +343,19 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma
// 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 == todo.Reword {
if action == "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.String()),
fmt.Sprintf("Updating rebase action of commit %s to '%s'", commit.ShortSha(), action),
false,
)
if err := self.git.Rebase.EditRebaseTodo(commit, action); err != nil {
if err := self.git.Rebase.EditRebaseTodo(
self.context().GetSelectedLineIdx(), action,
); err != nil {
return false, self.c.Error(err)
}
@@ -393,7 +383,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(commit); err != nil {
if err := self.git.Rebase.MoveTodoDown(index); err != nil {
return self.c.Error(err)
}
self.context().MoveSelectedLine(1)
@@ -431,7 +421,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error {
false,
)
if err := self.git.Rebase.MoveTodoUp(self.model.Commits[index]); err != nil {
if err := self.git.Rebase.MoveTodoDown(index - 1); err != nil {
return self.c.Error(err)
}
self.context().MoveSelectedLine(-1)
@@ -446,7 +436,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.MoveCommitUp(self.model.Commits, index)
err := self.git.Rebase.MoveCommitDown(self.model.Commits, index-1)
if err == nil {
self.context().MoveSelectedLine(-1)
}
@@ -472,7 +462,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(self.model.Commits, self.context().GetView().SelectedLineIdx())
err := self.git.Rebase.AmendTo(commit)
return self.helpers.MergeAndRebase.CheckMergeOrRebase(err)
})
},
@@ -480,10 +470,6 @@ 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{
@@ -774,16 +760,3 @@ 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)
}
+17 -9
View File
@@ -4,9 +4,15 @@ 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()
@@ -24,7 +30,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 == gocui.KeyEnter:
case key == newlineKey:
if allowMultiline {
textArea.TypeRune('\n')
} else {
@@ -60,20 +66,22 @@ 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, false)
v.RenderTextArea()
gui.RenderCommitLength()
return matched
}
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)
// 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)
}
v.RenderTextArea()
gui.RenderCommitLength()
return matched
}
func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
v.RenderTextArea()
+2 -2
View File
@@ -83,13 +83,13 @@ func (gui *Gui) filesRenderToMain() error {
return gui.c.RenderToMainViews(refreshOpts)
}
func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) {
func (gui *Gui) getSetTextareaTextFn(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.resizeCommitMessagePanels()
_ = gui.resizePopupPanel(view, view.TextArea.GetContent())
view.RenderTextArea()
}
}
+4
View File
@@ -195,6 +195,10 @@ 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
-8
View File
@@ -58,10 +58,6 @@ 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()
}
@@ -74,10 +70,6 @@ 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
View File
@@ -182,7 +182,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
}
initialContext := gui.currentSideContext()
if err := gui.c.ActivateContext(initialContext); err != nil {
if err := gui.c.PushContext(initialContext); err != nil {
return err
}
+14 -19
View File
@@ -4,7 +4,6 @@ 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"
@@ -264,9 +263,8 @@ func displayCommit(
bisectString := getBisectStatusText(bisectStatus, bisectInfo)
actionString := ""
if commit.Action != models.ActionNone {
todoString := commit.Action.String()
actionString = actionColorMap(commit.Action).Sprint(todoString) + " "
if commit.Action != "" {
actionString = actionColorMap(commit.Action).Sprint(commit.Action) + " "
}
tagString := ""
@@ -277,8 +275,6 @@ func displayCommit(
} else {
if len(commit.Tags) > 0 {
tagString = theme.DiffTerminalColor.SetBold().Sprint(strings.Join(commit.Tags, " ")) + " "
} else if common.UserConfig.Gui.ExperimentalShowBranchHeads && commit.ExtraInfo != "" {
tagString = style.FgMagenta.SetBold().Sprint("(*)") + " "
}
}
@@ -347,20 +343,19 @@ func getShaColor(
return getBisectStatusColor(bisectStatus)
}
diffed := commit.Sha != "" && commit.Sha == diffName
diffed := commit.Sha == diffName
shaColor := theme.DefaultTextColor
switch commit.Status {
case models.StatusUnpushed:
case "unpushed":
shaColor = style.FgRed
case models.StatusPushed:
case "pushed":
shaColor = style.FgYellow
case models.StatusMerged:
case "merged":
shaColor = style.FgGreen
case models.StatusRebasing:
case "rebasing":
shaColor = style.FgBlue
case models.StatusReflog:
case "reflog":
shaColor = style.FgBlue
default:
}
if diffed {
@@ -372,15 +367,15 @@ func getShaColor(
return shaColor
}
func actionColorMap(action todo.TodoCommand) style.TextStyle {
switch action {
case todo.Pick:
func actionColorMap(str string) style.TextStyle {
switch str {
case "pick":
return style.FgCyan
case todo.Drop:
case "drop":
return style.FgRed
case todo.Edit:
case "edit":
return style.FgGreen
case todo.Fixup:
case "fixup":
return style.FgMagenta
default:
return style.FgYellow
+13 -14
View File
@@ -5,7 +5,6 @@ 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"
@@ -92,8 +91,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
{
testName: "showing graph, including rebase commits",
commits: []*models.Commit{
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
{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"}},
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
@@ -115,8 +114,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: todo.Pick},
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
{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"}},
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
@@ -137,8 +136,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
{
testName: "startIdx is past TODO commits",
commits: []*models.Commit{
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
{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"}},
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
@@ -157,8 +156,8 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
{
testName: "only showing TODO commits",
commits: []*models.Commit{
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2", "sha3"}, Action: todo.Pick},
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}, Action: todo.Pick},
{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"}},
{Name: "commit4", Sha: "sha4", Parents: []string{"sha5"}},
{Name: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
@@ -196,10 +195,10 @@ func TestGetCommitListDisplayStrings(t *testing.T) {
{
testName: "only TODO commits except last",
commits: []*models.Commit{
{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: "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: "commit5", Sha: "sha5", Parents: []string{"sha7"}},
},
startIdx: 0,
@@ -216,7 +215,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: todo.Pick},
{Name: "commit1", Sha: "sha1", Parents: []string{"sha2"}, Action: "pick"},
{Name: "commit2", Sha: "sha2", Parents: []string{"sha3"}},
{Name: "commit3", Sha: "sha3", Parents: []string{"sha4"}},
},
-12
View File
@@ -85,7 +85,6 @@ func (gui *Gui) Refresh(options types.RefreshOptions) error {
types.REMOTES,
types.STATUS,
types.BISECT_INFO,
types.STAGING,
})
} else {
scopeSet = set.NewFromSlice(options.Scope)
@@ -564,13 +563,6 @@ 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
@@ -655,10 +647,6 @@ 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" {
-5
View File
@@ -42,14 +42,9 @@ 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()
+12 -23
View File
@@ -78,24 +78,28 @@ func (gui *Gui) resizeCurrentPopupPanel() error {
return nil
}
c := gui.c.CurrentContext()
if c == gui.State.Contexts.Menu {
if v == gui.Views.Menu {
gui.resizeMenu()
} else if c == gui.State.Contexts.Confirmation || c == gui.State.Contexts.Suggestions {
} else if v == gui.Views.Confirmation || v == gui.Views.Suggestions {
gui.resizeConfirmationPanel()
} else if c == gui.State.Contexts.CommitMessage || c == gui.State.Contexts.CommitDescription {
gui.resizeCommitMessagePanels()
} else if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(v, v.Buffer())
}
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.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
menuBottom := y1 - offset
_, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0)
@@ -117,7 +121,7 @@ func (gui *Gui) resizeConfirmationPanel() {
wrap = false
}
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
@@ -125,21 +129,6 @@ 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
+15 -24
View File
@@ -26,21 +26,20 @@ type Views struct {
PatchBuildingSecondary *gocui.View
MergeConflicts *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
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
// for playing the easter egg snake game
Snake *gocui.View
@@ -95,7 +94,6 @@ 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"},
@@ -201,17 +199,10 @@ func (gui *Gui) createAllViews() error {
gui.Views.AppStatus.Frame = false
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.c.Tr.CommitSummary
gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage
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
+4 -2
View File
@@ -47,7 +47,7 @@ func chineseTranslationSet() TranslationSet {
StagingTitle: "正在暂存",
MergingTitle: "正在合并",
NormalTitle: "正常",
CommitSummary: "提交信息",
CommitMessage: "提交信息",
CredentialsUsername: "用户名",
CredentialsPassword: "密码",
CredentialsPassphrase: "输入 SSH 密钥的密码",
@@ -94,7 +94,7 @@ func chineseTranslationSet() TranslationSet {
LcNewBranch: "新分支",
LcDeleteBranch: "删除分支",
NoBranchesThisRepo: "此仓库中没有分支",
CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认",
CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindNewLine}}:新行,{{.keyBindConfirm}}:确认",
CommitWithoutMessageErr: "您必须编写提交消息才能进行提交",
CloseConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认",
LcClose: "关闭",
@@ -201,6 +201,8 @@ func chineseTranslationSet() TranslationSet {
ReflogCommitsTitle: "Reflog 页面",
GlobalTitle: "全局键绑定",
ConflictsResolved: "已解决所有冲突。是否继续?",
RebasingTitle: "变基",
ConfirmRebase: "您确定要将分支 {{.checkedOutBranch}} 变基到 {{.selectedBranch}} 吗?",
ConfirmMerge: "您确定要将分支 {{.selectedBranch}} 合并到 {{.checkedOutBranch}} 吗?",
FwdNoUpstream: "此分支没有上游,无法快进",
FwdNoLocalUpstream: "此分支的远程未在本地注册,无法快进",
+4 -2
View File
@@ -13,7 +13,7 @@ func dutchTranslationSet() TranslationSet {
MainTitle: "Hoofd",
StagingTitle: "Staging",
NormalTitle: "Normaal",
CommitSummary: "Commitbericht",
CommitMessage: "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, {{.keyBindConfirm}}: Bevestig",
CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindNewLine}}: Nieuwe lijn, {{.keyBindConfirm}}: Bevestig",
CommitWithoutMessageErr: "Je kan geen commit maken zonder commit bericht",
CloseConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig",
LcClose: "sluiten",
@@ -166,7 +166,9 @@ 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",
+7 -19
View File
@@ -27,7 +27,7 @@ type TranslationSet struct {
MergeConfirmTitle string
NormalTitle string
LogTitle string
CommitSummary string
CommitMessage string
CredentialsUsername string
CredentialsPassword string
CredentialsPassphrase string
@@ -105,7 +105,6 @@ type TranslationSet struct {
SureResetCommitAuthor string
LcRenameCommitEditor string
NoCommitsThisBranch string
UpdateRefHere string
Error string
LcSelectHunk string
LcNavigateConflicts string
@@ -197,8 +196,6 @@ type TranslationSet struct {
MergeOptionsTitle string
RebaseOptionsTitle string
CommitMessageTitle string
CommitDescriptionTitle string
CommitDescriptionSubTitle string
LocalBranchesTitle string
SearchTitle string
TagsTitle string
@@ -211,9 +208,7 @@ type TranslationSet struct {
ReflogCommitsTitle string
ConflictsResolved string
RebasingTitle string
SimpleRebase string
InteractiveRebase string
InteractiveRebaseTooltip string
ConfirmRebase string
ConfirmMerge string
FwdNoUpstream string
FwdNoLocalUpstream string
@@ -223,7 +218,6 @@ type TranslationSet struct {
YouAreHere string
YouDied string
LcRewordNotSupported string
LcChangingThisActionIsNotAllowed string
LcCherryPickCopy string
LcCherryPickCopyRange string
LcPasteCommits string
@@ -700,7 +694,7 @@ func EnglishTranslationSet() TranslationSet {
MergingTitle: "Main Panel (Merging)",
NormalTitle: "Main Panel (Normal)",
LogTitle: "Log",
CommitSummary: "Commit summary",
CommitMessage: "Commit message",
CredentialsUsername: "Username",
CredentialsPassword: "Password",
CredentialsPassphrase: "Enter passphrase for SSH key",
@@ -752,7 +746,7 @@ func EnglishTranslationSet() TranslationSet {
LcNewBranch: "new branch",
LcDeleteBranch: "delete branch",
NoBranchesThisRepo: "No branches for this repo",
CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindNewLine}}: new line, {{.keyBindConfirm}}: confirm",
CommitWithoutMessageErr: "You cannot commit without a commit message",
CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm",
LcClose: "close",
@@ -760,7 +754,6 @@ 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",
@@ -868,9 +861,7 @@ func EnglishTranslationSet() TranslationSet {
RecentRepos: "recent repositories",
MergeOptionsTitle: "Merge Options",
RebaseOptionsTitle: "Rebase Options",
CommitMessageTitle: "Commit Summary",
CommitDescriptionTitle: "Commit description",
CommitDescriptionSubTitle: "Press tab to toggle focus",
CommitMessageTitle: "Commit Message",
LocalBranchesTitle: "Local Branches",
SearchTitle: "Search",
TagsTitle: "Tags",
@@ -883,10 +874,8 @@ func EnglishTranslationSet() TranslationSet {
ReflogCommitsTitle: "Reflog",
GlobalTitle: "Global Keybindings",
ConflictsResolved: "all merge conflicts resolved. Continue?",
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",
RebasingTitle: "Rebasing",
ConfirmRebase: "Are you sure you want to rebase '{{.checkedOutBranch}}' on top of '{{.selectedBranch}}'?",
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",
@@ -896,7 +885,6 @@ 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)",
+2 -2
View File
@@ -34,7 +34,7 @@ func japaneseTranslationSet() TranslationSet {
MergingTitle: "メインパネル (Merging)",
NormalTitle: "メインパネル (Normal)",
LogTitle: "ログ",
CommitSummary: "コミットメッセージ",
CommitMessage: "コミットメッセージ",
CredentialsUsername: "ユーザ名",
CredentialsPassword: "パスワード",
CredentialsPassphrase: "SSH鍵のパスフレーズを入力",
@@ -85,7 +85,7 @@ func japaneseTranslationSet() TranslationSet {
LcNewBranch: "新しいブランチを作成",
LcDeleteBranch: "ブランチを削除",
NoBranchesThisRepo: "リポジトリにブランチが存在しません",
CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindConfirm}}: 確定",
CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindNewLine}}: 改行, {{.keyBindConfirm}}: 確定",
CommitWithoutMessageErr: "コミットメッセージを入力してください",
CloseConfirm: "{{.keyBindClose}}: 閉じる/キャンセル, {{.keyBindConfirm}}: 確認",
LcClose: "閉じる",
+4 -2
View File
@@ -33,7 +33,7 @@ func koreanTranslationSet() TranslationSet {
MergingTitle: "메인 패널 (Merging)",
NormalTitle: "메인 패널 (Normal)",
LogTitle: "로그",
CommitSummary: "커밋 메시지",
CommitMessage: "커밋 메시지",
CredentialsUsername: "사용자 이름",
CredentialsPassword: "패스워드",
CredentialsPassphrase: "SSH키의 passphrase 입력",
@@ -84,7 +84,7 @@ func koreanTranslationSet() TranslationSet {
LcNewBranch: "새 브랜치 생성",
LcDeleteBranch: "브랜치 삭제",
NoBranchesThisRepo: "저장소에 브랜치가 존재하지 않습니다.",
CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindConfirm}}: 확인",
CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindNewLine}}: 개행, {{.keyBindConfirm}}: 확인",
CommitWithoutMessageErr: "커밋 메시지를 입력하세요.",
CloseConfirm: "{{.keyBindClose}}: 닫기/취소, {{.keyBindConfirm}}: 확인",
LcClose: "닫기",
@@ -202,6 +202,8 @@ 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",
+4 -2
View File
@@ -10,7 +10,7 @@ func polishTranslationSet() TranslationSet {
StashTitle: "Schowek",
UnstagedChanges: "Zmiany poza poczekalnią",
StagedChanges: "Zmiany w poczekalni",
CommitSummary: "Komunikat commita",
CommitMessage: "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, {{.keyBindConfirm}}: potwierdź",
CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindNewLine}}: nowa linia, {{.keyBindConfirm}}: potwierdź",
CommitWithoutMessageErr: "Nie możesz commitować bez komunikatu",
CloseConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
LcClose: "zamknij",
@@ -112,7 +112,9 @@ 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",
@@ -1,25 +0,0 @@
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,36 +10,19 @@ 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.Confirm)
self.t.press(self.t.keys.Universal.AppendNewline)
return self
}
@@ -66,20 +49,6 @@ 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
}
-13
View File
@@ -62,22 +62,9 @@ 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"
})
}
+1 -23
View File
@@ -8,7 +8,6 @@ import (
"path/filepath"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
@@ -43,11 +42,6 @@ func RunTests(
return err
}
gitVersion, err := getGitVersion()
if err != nil {
return err
}
for _, test := range tests {
test := test
@@ -57,7 +51,7 @@ func RunTests(
)
for i := 0; i < maxAttempts; i++ {
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, keyPressDelay, gitVersion)
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, keyPressDelay)
if err != nil {
if i == maxAttempts-1 {
return err
@@ -83,18 +77,12 @@ 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 {
@@ -156,16 +144,6 @@ 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()
+1 -5
View File
@@ -192,11 +192,7 @@ 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 {
return self.CreateNCommitsStartingAt(n, 1)
}
func (self *Shell) CreateNCommitsStartingAt(n, startIndex int) *Shell {
for i := startIndex; i < startIndex+n; i++ {
for i := 1; i <= n; i++ {
self.CreateFileAndAdd(
fmt.Sprintf("file%02d.txt", i),
fmt.Sprintf("file%02d content", i),
-58
View File
@@ -5,8 +5,6 @@ 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"
@@ -30,7 +28,6 @@ type IntegrationTest struct {
testDriver *TestDriver,
keys config.KeybindingConfig,
)
gitVersion GitVersionRestriction
}
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
@@ -48,56 +45,6 @@ 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 {
@@ -116,7 +63,6 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
setupRepo: args.SetupRepo,
setupConfig: args.SetupConfig,
run: args.Run,
gitVersion: args.GitVersion,
}
}
@@ -136,10 +82,6 @@ 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)
}
-63
View File
@@ -4,7 +4,6 @@ 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"
@@ -88,65 +87,3 @@ 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(&currentGitVersion)
assert.Equal(t, shouldRun, s.expectedShouldRun)
})
}
}
@@ -361,11 +361,6 @@ 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)
-4
View File
@@ -207,10 +207,6 @@ 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")
}
+3 -3
View File
@@ -30,9 +30,9 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Select(Contains("simple rebase")).
t.ExpectPopup().Confirmation().
Title(Equals("Rebasing")).
Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")).
Confirm()
t.Common().AcknowledgeConflicts()
@@ -36,9 +36,9 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Select(Contains("simple rebase")).
t.ExpectPopup().Confirmation().
Title(Equals("Rebasing")).
Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")).
Confirm()
t.Views().Information().Content(Contains("rebasing"))
@@ -39,9 +39,9 @@ var RebaseDoesNotAutosquash = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
Press(keys.Branches.RebaseBranch)
t.ExpectPopup().Menu().
Title(Equals("Rebase 'my-branch' onto 'master'")).
Select(Contains("simple rebase")).
t.ExpectPopup().Confirmation().
Title(Equals("Rebasing")).
Content(Contains("Are you sure you want to rebase 'my-branch' on top of 'master'?")).
Confirm()
t.Views().Commits().Lines(
@@ -22,20 +22,14 @@ var CommitMultiline = NewIntegrationTest(NewIntegrationTestArgs{
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Type("first line").
SwitchToDescription().
AddNewline().
AddNewline().
Type("fourth line").
SwitchToSummary().
Confirm()
t.ExpectPopup().CommitMessagePanel().Type("first line").AddNewline().AddNewline().Type("third line").Confirm()
t.Views().Commits().
Lines(
Contains("first line"),
)
t.Views().Commits().Focus()
t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*fourth line"))
t.Views().Main().Content(MatchesRegexp("first line\n\\s*\n\\s*third line"))
},
})
-53
View File
@@ -1,53 +0,0 @@
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(),
)
},
})
@@ -1,59 +0,0 @@
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"))
},
})
-66
View File
@@ -1,66 +0,0 @@
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,13 +25,6 @@ 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")).
@@ -5,7 +5,7 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var BasicCmdAtRuntime = NewIntegrationTest(NewIntegrationTestArgs{
var BasicAtRuntime = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using a custom command provided at runtime to create a new file",
ExtraCmdArgs: "",
Skip: false,
@@ -5,7 +5,7 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var OmitFromHistory = NewIntegrationTest(NewIntegrationTestArgs{
var BasicAtRuntimeHistory = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Omitting a runtime custom command from history if it begins with space",
ExtraCmdArgs: "",
Skip: false,
@@ -5,7 +5,7 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var BasicCmdFromConfig = NewIntegrationTest(NewIntegrationTestArgs{
var BasicFromConfig = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Using a custom command to create a new file",
ExtraCmdArgs: "",
Skip: false,
@@ -1,65 +0,0 @@
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),
)
},
})
@@ -1,50 +0,0 @@
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"))
},
})
@@ -1,60 +0,0 @@
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"),
)
},
})
@@ -1,62 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DropTodoCommitWithUpdateRefShowBranchHeads = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Drops a commit during interactive rebase when there is an update-ref in the git-rebase-todo file (with experimentalShowBranchHeads on)",
ExtraCmdArgs: "",
Skip: false,
GitVersion: From("2.38.0"),
SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ExperimentalShowBranchHeads = true
},
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(3).
NewBranch("mybranch").
CreateNCommitsStartingAt(3, 4)
shell.SetConfig("rebase.updateRefs", "true")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("(*) commit 06").IsSelected(),
Contains("commit 05"),
Contains("commit 04"),
Contains("(*) commit 03"),
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToLine(Contains("commit 01")).
Press(keys.Universal.Edit).
Focus().
Lines(
Contains("pick").Contains("(*) commit 06"),
Contains("pick").Contains("commit 05"),
Contains("pick").Contains("commit 04"),
Contains("update-ref").Contains("master"),
Contains("pick").Contains("(*) commit 03"),
Contains("pick").Contains("commit 02"),
Contains("<-- YOU ARE HERE --- commit 01"),
).
NavigateToLine(Contains("commit 05")).
Press(keys.Universal.Remove)
t.Common().ContinueRebase()
t.Views().Commits().
IsFocused().
Lines(
Contains("(*) commit 06"),
Contains("commit 04"),
Contains("(*) commit 03"),
Contains("commit 02"),
Contains("commit 01"),
)
},
})
@@ -27,8 +27,8 @@ var RewordFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.RenameCommit).
Tap(func() {
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Reword commit")).
t.ExpectPopup().Prompt().
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().CommitMessagePanel().
Title(Equals("Reword commit")).
t.ExpectPopup().Prompt().
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().CommitMessagePanel().
Title(Equals("Reword commit")).
t.ExpectPopup().Prompt().
Title(Equals("reword commit")).
InitialText(Equals("commit 02")).
Clear().
Type("renamed 02").
@@ -39,15 +39,11 @@ 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"))
+3 -10
View File
@@ -49,13 +49,10 @@ 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,
@@ -67,13 +64,13 @@ var tests = []*components.IntegrationTest{
conflicts.ResolveExternally,
conflicts.ResolveMultipleFiles,
conflicts.UndoChooseHunk,
custom_commands.BasicCmdAtRuntime,
custom_commands.BasicCmdFromConfig,
custom_commands.BasicAtRuntime,
custom_commands.BasicAtRuntimeHistory,
custom_commands.BasicFromConfig,
custom_commands.FormPrompts,
custom_commands.MenuFromCommand,
custom_commands.MenuFromCommandsOutput,
custom_commands.MultiplePrompts,
custom_commands.OmitFromHistory,
diff.Diff,
diff.DiffAndApplyPatch,
diff.DiffCommits,
@@ -86,14 +83,10 @@ 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.DropTodoCommitWithUpdateRefShowBranchHeads,
interactive_rebase.EditFirstCommit,
interactive_rebase.EditNonTodoCommitDuringRebase,
interactive_rebase.FixupFirstCommit,
-184
View File
@@ -1,184 +0,0 @@
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
}
-338
View File
@@ -1,338 +0,0 @@
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)
}
})
}
}
-21
View File
@@ -92,24 +92,3 @@ 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
}
-82
View File
@@ -233,85 +233,3 @@ 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)
})
})
}
-14
View File
@@ -1,14 +0,0 @@
#!/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
View File
@@ -1,42 +0,0 @@
// 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
}
+6 -26
View File
@@ -15,17 +15,14 @@ import (
const (
xsel = "xsel"
xclip = "xclip"
powershellExe = "powershell.exe"
clipExe = "clip.exe"
wlcopy = "wl-copy"
wlpaste = "wl-paste"
wlcopy = "wl-copy"
wlpaste = "wl-paste"
termuxClipboardGet = "termux-clipboard-get"
termuxClipboardSet = "termux-clipboard-set"
)
var (
Primary bool
trimDos bool
pasteCmdArgs []string
copyCmdArgs []string
@@ -36,11 +33,8 @@ 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}
@@ -50,8 +44,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 {
@@ -83,16 +77,6 @@ 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
}
@@ -119,11 +103,7 @@ func readAll() (string, error) {
if err != nil {
return "", err
}
result := string(out)
if trimDos && len(result) > 1 {
result = result[:len(result)-2]
}
return result, nil
return string(out), nil
}
func writeAll(text string) error {
+8 -37
View File
@@ -7,7 +7,6 @@
package clipboard
import (
"runtime"
"syscall"
"time"
"unsafe"
@@ -19,13 +18,12 @@ const (
)
var (
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")
user32 = syscall.MustLoadDLL("user32")
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")
@@ -52,27 +50,19 @@ 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
}
@@ -80,31 +70,21 @@ 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
}
@@ -114,7 +94,6 @@ 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() {
@@ -125,33 +104,25 @@ 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
View File
@@ -1,7 +0,0 @@
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.
+4 -21
View File
@@ -13,7 +13,6 @@ 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) {
@@ -56,11 +55,9 @@ func parseLine(line string) (Todo, error) {
fields := strings.Fields(line)
var commandLen int
for i := Pick; i < Comment; i++ {
for i := TodoCommand(Pick); i < Comment; i++ {
if isCommand(i, fields[0]) {
todo.Command = i
commandLen = len(fields[0])
todo.Command = TodoCommand(i)
fields = fields[1:]
break
}
@@ -76,14 +73,10 @@ func parseLine(line string) (Todo, error) {
}
if todo.Command == Label || todo.Command == Reset {
restOfLine := strings.TrimSpace(line[commandLen:])
if todo.Command == Reset && restOfLine == "[new root]" {
todo.Label = restOfLine
} else if len(fields) == 0 {
if len(fields) == 0 {
return todo, ErrMissingLabel
} else {
todo.Label = fields[0]
}
todo.Label = fields[0]
return todo, nil
}
@@ -97,7 +90,6 @@ 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
@@ -123,19 +115,10 @@ 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
}
+15 -20
View File
@@ -18,7 +18,6 @@ const (
NoOp
Drop
UpdateRef
Comment
)
@@ -28,12 +27,10 @@ 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 {
@@ -41,24 +38,23 @@ 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",
UpdateRef: "update-ref",
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",
Comment: "comment",
}
var todoCommandInfo = [15]struct {
var todoCommandInfo = [14]struct {
nickname string
cmd string
}{
@@ -76,5 +72,4 @@ var todoCommandInfo = [15]struct {
{"m", "merge"},
{"", "noop"},
{"d", "drop"},
{"u", "update-ref"},
}

Some files were not shown because too many files have changed in this diff Show More