Compare commits

..
Author SHA1 Message Date
Jesse Duffield 0a71b5f837 WIP 2024-11-23 11:56:10 +11:00
139 changed files with 748 additions and 3545 deletions
+6 -10
View File
@@ -3,7 +3,7 @@
Default path for the global config file:
- Linux: `~/.config/lazygit/config.yml`
- MacOS: `~/.config/lazygit/config.yml` (previously `~/Library/Application\ Support/lazygit/config.yml`)
- MacOS: `~/Library/Application\ Support/lazygit/config.yml`
- Windows: `%LOCALAPPDATA%\lazygit\config.yml` (default location, but it will also be found in `%APPDATA%\lazygit\config.yml`
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
@@ -12,6 +12,10 @@ For old installations (slightly embarrassing: I didn't realise at the time that
- MacOS: `~/Library/Application\ Support/jesseduffield/lazygit/config.yml`
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
If you want to change the config directory:
- MacOS: `export XDG_CONFIG_HOME="$HOME/.config"`
In addition to the global config file you can create repo-specific config files in `<repo>/.git/lazygit.yml`. Settings in these files override settings in the global config file. In addition, files called `.lazygit.yml` in any of the parent directories of a repo will also be loaded; this can be useful if you have settings that you want to apply to a group of repositories.
JSON schema is available for `config.yml` so that IntelliSense in Visual Studio Code (completion and error checking) is automatically enabled when the [YAML Red Hat][yaml] extension is installed. However, note that automatic schema detection only works if your config file is in one of the standard paths mentioned above. If you override the path to the file, you can still make IntelliSense work by adding
@@ -83,11 +87,6 @@ gui:
# - 'top': split the window vertically (side panel on top, main view below)
enlargedSideViewLocation: left
# If true, wrap lines in the staging view to the width of the view. This
# makes it much easier to work with diffs that have long lines, e.g.
# paragraphs of markdown text.
wrapLinesInStagingView: true
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
language: auto
@@ -165,9 +164,6 @@ gui:
# This can be toggled from within Lazygit with the '~' key, but that will not change the default.
showFileTree: true
# If true, show the number of lines changed per file in the Files view
showNumstatInFilesView: false
# If true, show a random tip in the command log when Lazygit starts
showRandomTip: true
@@ -328,7 +324,7 @@ git:
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --
# Command used to display git log of all branches in the main window.
# Deprecated: Use `allBranchesLogCmds` instead.
# Deprecated: User `allBranchesLogCmds` instead.
allBranchesLogCmd: git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium
# If true, do not spawn a separate process when using GPG
+5 -5
View File
@@ -16,7 +16,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20241223111608-9967d0e928a0
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@@ -38,7 +38,7 @@ require (
github.com/stretchr/testify v1.8.1
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8
golang.org/x/sync v0.10.0
golang.org/x/sync v0.8.0
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -75,8 +75,8 @@ require (
github.com/xanzy/ssh-agent v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
+10 -10
View File
@@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20241223111608-9967d0e928a0 h1:R29+E15wHqTDBfZxmzCLu0x34j5ljsXWT/DhR+2YiOU=
github.com/jesseduffield/gocui v0.3.1-0.20241223111608-9967d0e928a0/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f h1:ZzsAUDwPFLPITKLcJpMSqt/3rERdI8YRZKr2l0plrls=
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@@ -424,8 +424,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -475,14 +475,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -493,8 +493,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+8 -30
View File
@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
)
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
@@ -38,7 +39,6 @@ const (
DaemonKindMoveTodosDown
DaemonKindInsertBreak
DaemonKindChangeTodoActions
DaemonKindDropMergeCommit
DaemonKindMoveFixupCommitDown
DaemonKindWriteRebaseTodo
)
@@ -58,7 +58,6 @@ func getInstruction() Instruction {
DaemonKindRemoveUpdateRefsForCopiedBranch: deserializeInstruction[*RemoveUpdateRefsForCopiedBranchInstruction],
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
DaemonKindDropMergeCommit: deserializeInstruction[*DropMergeCommitInstruction],
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
DaemonKindMoveTodosUp: deserializeInstruction[*MoveTodosUpInstruction],
DaemonKindMoveTodosDown: deserializeInstruction[*MoveTodosDownInstruction],
@@ -236,6 +235,7 @@ func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
changes := lo.Map(self.Changes, func(c ChangeTodoAction, _ int) utils.TodoChange {
return utils.TodoChange{
Hash: c.Hash,
OldAction: todo.Pick,
NewAction: c.NewAction,
}
})
@@ -244,30 +244,6 @@ func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
})
}
type DropMergeCommitInstruction struct {
Hash string
}
func NewDropMergeCommitInstruction(hash string) Instruction {
return &DropMergeCommitInstruction{
Hash: hash,
}
}
func (self *DropMergeCommitInstruction) Kind() DaemonKind {
return DaemonKindDropMergeCommit
}
func (self *DropMergeCommitInstruction) SerializedInstructions() string {
return serializeInstruction(self)
}
func (self *DropMergeCommitInstruction) run(common *common.Common) error {
return handleInteractiveRebase(common, func(path string) error {
return utils.DropMergeCommit(path, self.Hash, getCommentChar())
})
}
// Takes the hash of some commit, and the hash of a fixup commit that was created
// at the end of the branch, then moves the fixup commit down to right after the
// original commit, changing its type to "fixup" (only if ChangeToFixup is true)
@@ -320,12 +296,13 @@ func (self *MoveTodosUpInstruction) SerializedInstructions() string {
func (self *MoveTodosUpInstruction) run(common *common.Common) error {
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
return utils.Todo{
Hash: hash,
Hash: hash,
Action: todo.Pick,
}
})
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodosUp(path, todosToMove, false, getCommentChar())
return utils.MoveTodosUp(path, todosToMove, getCommentChar())
})
}
@@ -350,12 +327,13 @@ func (self *MoveTodosDownInstruction) SerializedInstructions() string {
func (self *MoveTodosDownInstruction) run(common *common.Common) error {
todosToMove := lo.Map(self.Hashes, func(hash string, _ int) utils.Todo {
return utils.Todo{
Hash: hash,
Hash: hash,
Action: todo.Pick,
}
})
return handleInteractiveRebase(common, func(path string) error {
return utils.MoveTodosDown(path, todosToMove, false, getCommentChar())
return utils.MoveTodosDown(path, todosToMove, getCommentChar())
})
}
+2 -2
View File
@@ -109,10 +109,10 @@ func (self *BranchCommands) CurrentBranchName() (string, error) {
}
// LocalDelete delete branch locally
func (self *BranchCommands) LocalDelete(branches []string, force bool) error {
func (self *BranchCommands) LocalDelete(branch string, force bool) error {
cmdArgs := NewGitCmd("branch").
ArgIfElse(force, "-D", "-d").
Arg(branches...).
Arg(branch).
ToArgv()
return self.cmd.New(cmdArgs).Run()
+5 -26
View File
@@ -62,57 +62,36 @@ func TestBranchNewBranch(t *testing.T) {
func TestBranchDeleteBranch(t *testing.T) {
type scenario struct {
testName string
branchNames []string
force bool
runner *oscommands.FakeCmdObjRunner
test func(error)
testName string
force bool
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"Delete a branch",
[]string{"test"},
false,
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-d", "test"}, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Delete multiple branches",
[]string{"test1", "test2", "test3"},
false,
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-d", "test1", "test2", "test3"}, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete a branch",
[]string{"test"},
true,
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-D", "test"}, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete multiple branches",
[]string{"test1", "test2", "test3"},
true,
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"branch", "-D", "test1", "test2", "test3"}, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
instance := buildBranchCommands(commonDeps{runner: s.runner})
s.test(instance.LocalDelete(s.branchNames, s.force))
s.test(instance.LocalDelete("test", s.force))
s.runner.CheckForMissingCalls()
})
}
-3
View File
@@ -19,7 +19,6 @@ func NewDiffCommands(gitCommon *GitCommon) *DiffCommands {
func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
extDiffCmd := self.UserConfig().Git.Paging.ExternalDiffCommand
useExtDiff := extDiffCmd != ""
ignoreWhitespace := self.AppState.IgnoreWhitespaceInDiffView
return self.cmd.New(
NewGitCmd("diff").
@@ -28,8 +27,6 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) oscommands.ICmdObj {
ArgIfElse(useExtDiff, "--ext-diff", "--no-ext-diff").
Arg("--submodule").
Arg(fmt.Sprintf("--color=%s", self.UserConfig().Git.Paging.ColorArg)).
ArgIf(ignoreWhitespace, "--ignore-all-space").
Arg(fmt.Sprintf("--unified=%d", self.AppState.DiffContextSize)).
Arg(diffArgs...).
Dir(self.repoPaths.worktreePath).
ToArgv(),
-63
View File
@@ -3,7 +3,6 @@ package git_commands
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -49,14 +48,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
}
files := []*models.File{}
fileDiffs := map[string]FileDiff{}
if self.GitCommon.Common.UserConfig().Gui.ShowNumstatInFilesView {
fileDiffs, err = self.getFileDiffs()
if err != nil {
self.Log.Error(err)
}
}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
@@ -69,11 +60,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
DisplayString: status.StatusString,
}
if diff, ok := fileDiffs[status.Name]; ok {
file.LinesAdded = diff.LinesAdded
file.LinesDeleted = diff.LinesDeleted
}
models.SetStatusFields(file, status.Change)
files = append(files, file)
}
@@ -101,45 +87,6 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
return files
}
type FileDiff struct {
LinesAdded int
LinesDeleted int
}
func (fileLoader *FileLoader) getFileDiffs() (map[string]FileDiff, error) {
diffs, err := fileLoader.gitDiffNumStat()
if err != nil {
return nil, err
}
splitLines := strings.Split(diffs, "\x00")
fileDiffs := map[string]FileDiff{}
for _, line := range splitLines {
splitLine := strings.Split(line, "\t")
if len(splitLine) != 3 {
continue
}
linesAdded, err := strconv.Atoi(splitLine[0])
if err != nil {
continue
}
linesDeleted, err := strconv.Atoi(splitLine[1])
if err != nil {
continue
}
fileName := splitLine[2]
fileDiffs[fileName] = FileDiff{
LinesAdded: linesAdded,
LinesDeleted: linesDeleted,
}
}
return fileDiffs, nil
}
// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
@@ -153,16 +100,6 @@ type FileStatus struct {
PreviousName string
}
func (fileLoader *FileLoader) gitDiffNumStat() (string, error) {
return fileLoader.cmd.New(
NewGitCmd("diff").
Arg("--numstat").
Arg("-z").
Arg("HEAD").
ToArgv(),
).DontLog().RunWithOutput()
}
func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
cmdArgs := NewGitCmd("status").
Arg(opts.UntrackedFilesArg).
+25 -47
View File
@@ -11,35 +11,29 @@ import (
func TestFileGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
similarityThreshold int
runner oscommands.ICmdObjRunner
showNumstatInFilesView bool
expectedFiles []*models.File
testName string
similarityThreshold int
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
testName: "No files found",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
"No files found",
50,
oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil),
expectedFiles: []*models.File{},
[]*models.File{},
},
{
testName: "Several files found",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
"Several files found",
50,
oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
).
ExpectGitArgs([]string{"diff", "--numstat", "-z", "HEAD"},
"4\t1\tfile1.txt\x001\t0\tfile2.txt\x002\t2\tfile3.txt\x000\t2\tfile4.txt\x002\t2\tfile5.txt",
nil,
),
showNumstatInFilesView: true,
expectedFiles: []*models.File{
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
@@ -51,8 +45,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
ShortStatus: "MM",
LinesAdded: 4,
LinesDeleted: 1,
},
{
Name: "file3.txt",
@@ -65,8 +57,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
ShortStatus: "A ",
LinesAdded: 2,
LinesDeleted: 2,
},
{
Name: "file2.txt",
@@ -79,8 +69,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
ShortStatus: "AM",
LinesAdded: 1,
LinesDeleted: 0,
},
{
Name: "file4.txt",
@@ -93,8 +81,6 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
ShortStatus: "??",
LinesAdded: 0,
LinesDeleted: 2,
},
{
Name: "file5.txt",
@@ -107,17 +93,15 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
ShortStatus: "UU",
LinesAdded: 2,
LinesDeleted: 2,
},
},
},
{
testName: "File with new line char",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
"File with new line char",
50,
oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM a\nb.txt", nil),
expectedFiles: []*models.File{
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
@@ -133,14 +117,14 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
{
testName: "Renamed files",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
"Renamed files",
50,
oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
expectedFiles: []*models.File{
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
@@ -170,14 +154,14 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
{
testName: "File with arrow in name",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
"File with arrow in name",
50,
oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
`?? a -> b.txt`,
nil,
),
expectedFiles: []*models.File{
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
@@ -201,14 +185,8 @@ func TestFileGetStatusFiles(t *testing.T) {
appState := &config.AppState{}
appState.RenameSimilarityThreshold = s.similarityThreshold
userConfig := &config.UserConfig{
Gui: config.GuiConfig{
ShowNumstatInFilesView: s.showNumstatInFilesView,
},
}
loader := &FileLoader{
GitCommon: buildGitCommon(commonDeps{appState: appState, userConfig: userConfig}),
GitCommon: buildGitCommon(commonDeps{appState: appState}),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
+7 -13
View File
@@ -145,11 +145,11 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx
baseHashOrRoot := getBaseHashOrRoot(commits, baseIndex)
changes := lo.FilterMap(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) (daemon.ChangeTodoAction, bool) {
changes := lo.Map(commits[startIdx:endIdx+1], func(commit *models.Commit, _ int) daemon.ChangeTodoAction {
return daemon.ChangeTodoAction{
Hash: commit.Hash,
NewAction: action,
}, !commit.IsMerge()
}
})
self.os.LogCommand(logTodoChanges(changes), false)
@@ -324,9 +324,9 @@ func (self *RebaseCommands) MoveFixupCommitDown(commits []*models.Commit, target
func todoFromCommit(commit *models.Commit) utils.Todo {
if commit.Action == todo.UpdateRef {
return utils.Todo{Ref: commit.Name}
return utils.Todo{Ref: commit.Name, Action: commit.Action}
} else {
return utils.Todo{Hash: commit.Hash}
return utils.Todo{Hash: commit.Hash, Action: commit.Action}
}
}
@@ -335,6 +335,7 @@ func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo
commitsWithAction := lo.Map(commits, func(commit *models.Commit, _ int) utils.TodoChange {
return utils.TodoChange{
Hash: commit.Hash,
OldAction: commit.Action,
NewAction: action,
}
})
@@ -369,7 +370,7 @@ func (self *RebaseCommands) MoveTodosDown(commits []*models.Commit) error {
return todoFromCommit(commit)
})
return utils.MoveTodosDown(fileName, todosToMove, true, self.config.GetCoreCommentChar())
return utils.MoveTodosDown(fileName, todosToMove, self.config.GetCoreCommentChar())
}
func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error {
@@ -378,7 +379,7 @@ func (self *RebaseCommands) MoveTodosUp(commits []*models.Commit) error {
return todoFromCommit(commit)
})
return utils.MoveTodosUp(fileName, todosToMove, true, self.config.GetCoreCommentChar())
return utils.MoveTodosUp(fileName, todosToMove, self.config.GetCoreCommentChar())
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
@@ -564,13 +565,6 @@ func (self *RebaseCommands) CherryPickCommitsDuringRebase(commits []*models.Comm
return utils.PrependStrToTodoFile(filePath, []byte(todo))
}
func (self *RebaseCommands) DropMergeCommit(commits []*models.Commit, commitIndex int) error {
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
baseHashOrRoot: getBaseHashOrRoot(commits, commitIndex+1),
instruction: daemon.NewDropMergeCommitInstruction(commits[commitIndex].Hash),
}).Run()
}
// we can't start an interactive rebase from the first commit without passing the
// '--root' arg
func getBaseHashOrRoot(commits []*models.Commit, index int) string {
+2 -3
View File
@@ -49,10 +49,9 @@ func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string
return self.cmd.New(cmdArgs).Run()
}
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchNames []string) error {
func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName string, branchName string) error {
cmdArgs := NewGitCmd("push").
Arg(remoteName, "--delete").
Arg(branchNames...).
Arg(remoteName, "--delete", branchName).
ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
-2
View File
@@ -19,8 +19,6 @@ type File struct {
HasInlineMergeConflicts bool
DisplayString string
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
LinesDeleted int
LinesAdded int
// If true, this must be a worktree folder
IsWorktree bool
+1 -1
View File
@@ -52,7 +52,7 @@ func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
}
func (self *CmdObjBuilder) NewInteractiveShell(commandStr string) ICmdObj {
quotedCommand := self.quotedCommandString(commandStr + self.platform.InteractiveShellExit)
quotedCommand := self.quotedCommandString(commandStr)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s %s", self.platform.InteractiveShell, self.platform.InteractiveShellArg, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
+7 -8
View File
@@ -51,14 +51,13 @@ func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
}
var dummyPlatform = &Platform{
OS: "darwin",
Shell: "bash",
InteractiveShell: "bash",
ShellArg: "-c",
InteractiveShellArg: "-i",
InteractiveShellExit: "; exit $?",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
OS: "darwin",
Shell: "bash",
InteractiveShell: "bash",
ShellArg: "-c",
InteractiveShellArg: "-i",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
+7 -8
View File
@@ -35,14 +35,13 @@ type OSCommand struct {
// Platform stores the os state
type Platform struct {
OS string
Shell string
InteractiveShell string
ShellArg string
InteractiveShellArg string
InteractiveShellExit string
OpenCommand string
OpenLinkCommand string
OS string
Shell string
InteractiveShell string
ShellArg string
InteractiveShellArg string
OpenCommand string
OpenLinkCommand string
}
// NewOSCommand os command runner
@@ -10,14 +10,13 @@ import (
func GetPlatform() *Platform {
return &Platform{
OS: runtime.GOOS,
Shell: "bash",
InteractiveShell: getUserShell(),
ShellArg: "-c",
InteractiveShellArg: "-i",
InteractiveShellExit: "; exit $?",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
OS: runtime.GOOS,
Shell: "bash",
InteractiveShell: getUserShell(),
ShellArg: "-c",
InteractiveShellArg: "-i",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
}
+5 -6
View File
@@ -2,11 +2,10 @@ package oscommands
func GetPlatform() *Platform {
return &Platform{
OS: "windows",
Shell: "cmd",
InteractiveShell: "cmd",
ShellArg: "/c",
InteractiveShellArg: "",
InteractiveShellExit: "",
OS: "windows",
Shell: "cmd",
InteractiveShell: "cmd",
ShellArg: "/c",
InteractiveShellArg: "",
}
}
+1 -36
View File
@@ -349,34 +349,13 @@ func findConfigFile(filename string) (exists bool, path string) {
return true, legacyConfigPath
}
// if on macOs, default to looking in ~/.config/lazygit first since that's
// the most common location for CLI apps to store configuration when
// `XDG_CONFIG_HOME` is not set. The `xdg` package uses `~/Library/Application\ Support`
// as the default directory for macOS, so we have to handle this case manually.
if overrideXdgConfigHome && os.Getenv("XDG_CONFIG_HOME") == "" {
homeConfigFilePath, err := findHomeConfigFile(filename)
if err == nil {
return true, homeConfigFilePath
}
}
// look for lazygit/filename in XDG_CONFIG_HOME and XDG_CONFIG_DIRS
configFilepath, err := xdg.SearchConfigFile(filepath.Join("lazygit", filename))
if err == nil {
return true, configFilepath
}
configHome := xdg.ConfigHome
// If we're on macOS and XDG_CONFIG_HOME is not set, we use
// ~/.config/lazygit instead of the default `~/Library/Application\ Support`
// directory the `xdg` package uses.
if overrideXdgConfigHome && os.Getenv("XDG_CONFIG_HOME") == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
return false, filepath.Join(homeDir, ".config", "lazygit", filename)
}
}
return false, filepath.Join(configHome, "lazygit", filename)
return false, filepath.Join(xdg.ConfigHome, "lazygit", filename)
}
var ConfigFilename = "config.yml"
@@ -515,17 +494,3 @@ func LogPath() (string, error) {
return stateFilePath("development.log")
}
func findHomeConfigFile(filename string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
path := filepath.Join(home, ".config", "lazygit", filename)
if _, err = os.Stat(path); err != nil {
return "", err
}
return path, nil
}
+2 -2
View File
@@ -21,8 +21,8 @@ func isContainer() bool {
func GetPlatformDefaultConfig() OSConfig {
if isWSL() && !isContainer() {
return OSConfig{
Open: `powershell.exe start explorer.exe "$(wslpath -w {{filename}})" >/dev/null`,
OpenLink: `powershell.exe start '{{link}}' >/dev/null`,
Open: `powershell.exe start explorer.exe {{filename}} >/dev/null`,
OpenLink: `powershell.exe start {{link}} >/dev/null`,
}
}
+1 -9
View File
@@ -91,10 +91,6 @@ type GuiConfig struct {
// - 'left': split the window horizontally (side panel on the left, main view on the right)
// - 'top': split the window vertically (side panel on top, main view below)
EnlargedSideViewLocation string `yaml:"enlargedSideViewLocation"`
// If true, wrap lines in the staging view to the width of the view. This
// makes it much easier to work with diffs that have long lines, e.g.
// paragraphs of markdown text.
WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"`
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru'
Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"`
// Format used when displaying time e.g. commit time.
@@ -113,8 +109,6 @@ type GuiConfig struct {
// If true, display the files in the file views as a tree. If false, display the files as a flat list.
// This can be toggled from within Lazygit with the '~' key, but that will not change the default.
ShowFileTree bool `yaml:"showFileTree"`
// If true, show the number of lines changed per file in the Files view
ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"`
// If true, show a random tip in the command log when Lazygit starts
ShowRandomTip bool `yaml:"showRandomTip"`
// If true, show the command log
@@ -244,7 +238,7 @@ type GitConfig struct {
// Command used when displaying the current branch git log in the main window
BranchLogCmd string `yaml:"branchLogCmd"`
// Command used to display git log of all branches in the main window.
// Deprecated: Use `allBranchesLogCmds` instead.
// Deprecated: User `allBranchesLogCmds` instead.
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
// Commands used to display git log of all branches in the main window, they will be cycled in order of appearance
AllBranchesLogCmds []string `yaml:"allBranchesLogCmds"`
@@ -696,7 +690,6 @@ func GetDefaultConfig() *UserConfig {
ExpandedSidePanelWeight: 2,
MainPanelSplitMode: "flexible",
EnlargedSideViewLocation: "left",
WrapLinesInStagingView: true,
Language: "auto",
TimeFormat: "02 Jan 06",
ShortTimeFormat: time.Kitchen,
@@ -721,7 +714,6 @@ func GetDefaultConfig() *UserConfig {
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowNumstatInFilesView: false,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
-5
View File
@@ -1,5 +0,0 @@
//go:build !darwin
package config
const overrideXdgConfigHome = false
-5
View File
@@ -1,5 +0,0 @@
//go:build darwin
package config
const overrideXdgConfigHome = true
+15 -11
View File
@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"runtime"
"strings"
"time"
"github.com/jesseduffield/gocui"
@@ -75,18 +76,21 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
func (self *BackgroundRoutineMgr) startBackgroundFetch() {
self.gui.waitForIntro.Wait()
fetch := func() error {
err := self.backgroundFetch()
self.gui.c.Render()
return err
}
// We want an immediate fetch at startup, and since goEvery starts by
// waiting for the interval, we need to trigger one manually first
_ = fetch()
isNew := self.gui.IsNewRepo
userConfig := self.gui.UserConfig()
self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, fetch)
if !isNew {
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
}
err := self.backgroundFetch()
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
self.gui.c.Alert(self.gui.c.Tr.NoAutomaticGitFetchTitle, self.gui.c.Tr.NoAutomaticGitFetchBody)
} else {
self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error {
err := self.backgroundFetch()
self.gui.c.Render()
return err
})
}
}
func (self *BackgroundRoutineMgr) startBackgroundFilesRefresh(refreshInterval int) {
+2
View File
@@ -70,6 +70,8 @@ func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) {
return
}
gui.resetHelpersAndControllers()
contextsToDeactivate, contextToActivate := self.pushToContextStack(c)
for _, contextToDeactivate := range contextsToDeactivate {
+1 -1
View File
@@ -30,7 +30,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.State().GetItemOperation,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Views().Branches.InnerWidth(),
c.Views().Branches.Width(),
c.Tr,
c.UserConfig(),
c.Model().Worktrees,
@@ -30,9 +30,6 @@ type CommitMessageViewModel struct {
// 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
// we remember the initial message so that we can tell whether we should preserve
// the message; if it's still identical to the initial message, we don't
initialMessage string
// the full preserved message (combined summary and description)
preservedMessage string
// invoked when pressing enter in the commit message panel
@@ -87,10 +84,6 @@ func (self *CommitMessageContext) SetPreservedMessage(message string) {
self.viewModel.preservedMessage = message
}
func (self *CommitMessageContext) GetInitialMessage() string {
return strings.TrimSpace(self.viewModel.initialMessage)
}
func (self *CommitMessageContext) GetHistoryMessage() string {
return self.viewModel.historyMessage
}
@@ -108,13 +101,11 @@ func (self *CommitMessageContext) SetPanelState(
summaryTitle string,
descriptionTitle string,
preserveMessage bool,
initialMessage string,
onConfirm func(string, string) error,
onSwitchToEditor func(string) error,
) {
self.viewModel.selectedindex = index
self.viewModel.preserveMessage = preserveMessage
self.viewModel.initialMessage = initialMessage
self.viewModel.onConfirm = onConfirm
self.viewModel.onSwitchToEditor = onSwitchToEditor
self.GetView().Title = summaryTitle
+1
View File
@@ -14,6 +14,7 @@ type ConfirmationContext struct {
type ConfirmationContextState struct {
OnConfirm func() error
OnClose func() error
Multiline bool
}
var _ types.Context = (*ConfirmationContext)(nil)
+1 -1
View File
@@ -115,5 +115,5 @@ func (self *MergeConflictsContext) SetSelectedLineRange() {
func (self *MergeConflictsContext) GetOriginY() int {
view := self.GetView()
conflictMiddle := self.GetState().GetConflictMiddle()
return int(math.Max(0, float64(conflictMiddle-(view.InnerHeight()/2))))
return int(math.Max(0, float64(conflictMiddle-(view.Height()/2))))
}
+10 -20
View File
@@ -39,13 +39,12 @@ func NewPatchExplorerContext(
mutex: &deadlock.Mutex{},
getIncludedLineIndices: getIncludedLineIndices,
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
HighlightOnFocus: true,
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES,
View: view,
WindowName: windowName,
Key: key,
Kind: types.MAIN_CONTEXT,
Focusable: true,
HighlightOnFocus: true,
})),
SearchTrait: NewSearchTrait(c),
}
@@ -59,8 +58,6 @@ func NewPatchExplorerContext(
}),
)
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
return ctx
}
@@ -107,15 +104,16 @@ func (self *PatchExplorerContext) setContent() {
func (self *PatchExplorerContext) FocusSelection() {
view := self.GetView()
state := self.GetState()
bufferHeight := view.InnerHeight()
_, viewHeight := view.Size()
bufferHeight := viewHeight - 1
_, origin := view.Origin()
numLines := view.ViewLinesHeight()
numLines := view.LinesHeight()
newOriginY := state.CalculateOrigin(origin, bufferHeight, numLines)
view.SetOriginY(newOriginY)
startIdx, endIdx := state.SelectedViewRange()
startIdx, endIdx := state.SelectedRange()
// As far as the view is concerned, we are always selecting a range
view.SetRangeSelectStart(startIdx)
view.SetCursorY(endIdx - newOriginY)
@@ -143,11 +141,3 @@ func (self *PatchExplorerContext) GetMutex() *deadlock.Mutex {
func (self *PatchExplorerContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
return nil
}
func (self *PatchExplorerContext) OnViewWidthChanged() {
if state := self.GetState(); state != nil {
state.OnViewWidthChanged(self.GetView())
self.setContent()
self.RenderAndFocus()
}
}
-8
View File
@@ -7,7 +7,6 @@ import (
type SimpleContext struct {
*BaseContext
handleRenderFunc func()
}
func NewSimpleContext(baseContext *BaseContext) *SimpleContext {
@@ -55,13 +54,6 @@ func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) {
}
func (self *SimpleContext) HandleRender() {
if self.handleRenderFunc != nil {
self.handleRenderFunc()
}
}
func (self *SimpleContext) SetHandleRenderFunc(f func()) {
self.handleRenderFunc = f
}
func (self *SimpleContext) HandleRenderToMain() {
+2 -2
View File
@@ -63,7 +63,7 @@ func (self *ViewTrait) SetOriginX(value int) {
// tells us the start of line indexes shown in the view currently as well as the capacity of lines shown in the viewport.
func (self *ViewTrait) ViewPortYBounds() (int, int) {
_, start := self.view.Origin()
length := self.view.InnerHeight()
length := self.view.InnerHeight() + 1
return start, length
}
@@ -89,7 +89,7 @@ func (self *ViewTrait) ScrollDown(value int) {
// this returns the amount we'll scroll if we want to scroll by a page.
func (self *ViewTrait) PageDelta() int {
height := self.view.InnerHeight()
_, height := self.view.Size()
delta := height - 1
if delta == 0 {
+1 -2
View File
@@ -30,8 +30,7 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
getDisplayStrings := func(_ int, _ int) [][]string {
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat)
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
+1 -1
View File
@@ -299,7 +299,7 @@ func (gui *Gui) resetHelpersAndControllers() {
)
controllers.AttachControllers(gui.State.Contexts.CustomPatchBuilderSecondary,
verticalScrollControllerFactory.Create(gui.State.Contexts.CustomPatchBuilderSecondary),
verticalScrollControllerFactory.Create(gui.State.Contexts.CustomPatchBuilder),
)
controllers.AttachControllers(gui.State.Contexts.MergeConflicts,
+31 -91
View File
@@ -91,8 +91,8 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.withItems(self.delete),
GetDisabledReason: self.require(self.itemRangeSelected(self.branchesAreReal)),
Handler: self.withItem(self.delete),
GetDisabledReason: self.require(self.singleItemSelected(self.branchIsReal)),
Description: self.c.Tr.Delete,
Tooltip: self.c.Tr.BranchDeleteTooltip,
OpensMenu: true,
@@ -520,80 +520,62 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, KeepBranchSelectionIndex: true})
}
func (self *BranchesController) localDelete(branches []*models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branches)
func (self *BranchesController) localDelete(branch *models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmLocalDelete(branch)
}
func (self *BranchesController) remoteDelete(branches []*models.Branch) error {
remoteBranches := lo.Map(branches, func(branch *models.Branch, _ int) *models.RemoteBranch {
return &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
})
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(remoteBranches)
func (self *BranchesController) remoteDelete(branch *models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(branch.UpstreamRemote, branch.UpstreamBranch)
}
func (self *BranchesController) localAndRemoteDelete(branches []*models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branches)
func (self *BranchesController) localAndRemoteDelete(branch *models.Branch) error {
return self.c.Helpers().BranchesHelper.ConfirmLocalAndRemoteDelete(branch)
}
func (self *BranchesController) delete(branches []*models.Branch) error {
func (self *BranchesController) delete(branch *models.Branch) error {
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef()
isBranchCheckedOut := lo.SomeBy(branches, func(branch *models.Branch) bool {
return checkedOutBranch.Name == branch.Name
})
hasUpstream := lo.EveryBy(branches, func(branch *models.Branch) bool {
return branch.IsTrackingRemote() && !branch.UpstreamGone
})
localDeleteItem := &types.MenuItem{
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalBranches, self.c.Tr.DeleteLocalBranch),
Label: self.c.Tr.DeleteLocalBranch,
Key: 'c',
OnPress: func() error {
return self.localDelete(branches)
return self.localDelete(branch)
},
}
if isBranchCheckedOut {
if checkedOutBranch.Name == branch.Name {
localDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
}
remoteDeleteItem := &types.MenuItem{
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteRemoteBranches, self.c.Tr.DeleteRemoteBranch),
Label: self.c.Tr.DeleteRemoteBranch,
Key: 'r',
OnPress: func() error {
return self.remoteDelete(branches)
return self.remoteDelete(branch)
},
}
if !hasUpstream {
remoteDeleteItem.DisabledReason = &types.DisabledReason{
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
}
if !branch.IsTrackingRemote() || branch.UpstreamGone {
remoteDeleteItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
}
deleteBothItem := &types.MenuItem{
Label: lo.Ternary(len(branches) > 1, self.c.Tr.DeleteLocalAndRemoteBranches, self.c.Tr.DeleteLocalAndRemoteBranch),
Label: self.c.Tr.DeleteLocalAndRemoteBranch,
Key: 'b',
OnPress: func() error {
return self.localAndRemoteDelete(branches)
return self.localAndRemoteDelete(branch)
},
}
if isBranchCheckedOut {
if checkedOutBranch.Name == branch.Name {
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.CantDeleteCheckOutBranch}
} else if !hasUpstream {
deleteBothItem.DisabledReason = &types.DisabledReason{
Text: lo.Ternary(len(branches) > 1, self.c.Tr.UpstreamsNotSetError, self.c.Tr.UpstreamNotSetError),
}
} else if !branch.IsTrackingRemote() || branch.UpstreamGone {
deleteBothItem.DisabledReason = &types.DisabledReason{Text: self.c.Tr.UpstreamNotSetError}
}
var menuTitle string
if len(branches) == 1 {
menuTitle = utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{
"selectedBranchName": branches[0].Name,
},
)
} else {
menuTitle = self.c.Tr.DeleteBranchesTitle
}
menuTitle := utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{
"selectedBranchName": branch.Name,
},
)
return self.c.Menu(types.CreateMenuOptions{
Title: menuTitle,
@@ -748,23 +730,11 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
{
LabelColumns: fromToLabelColumns(branch.Name, self.c.Tr.SelectBranch),
OnPress: func() error {
if !branch.IsTrackingRemote() {
return errors.New(self.c.Tr.PullRequestNoUpstream)
}
if len(self.c.Model().Remotes) == 1 {
toRemote := self.c.Model().Remotes[0].Name
self.c.Log.Debugf("PR will target the only existing remote '%s'", toRemote)
return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
}
self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.SelectTargetRemote,
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(),
HandleConfirm: func(toRemote string) error {
self.c.Log.Debugf("PR will target remote '%s'", toRemote)
return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
Title: branch.Name + " →",
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesSuggestionsFunc("/"),
HandleConfirm: func(targetBranchName string) error {
return self.createPullRequest(branch.Name, targetBranchName)
},
})
@@ -794,26 +764,6 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
return self.c.Menu(types.CreateMenuOptions{Title: fmt.Sprint(self.c.Tr.CreatePullRequestOptions), Items: menuItems})
}
func (self *BranchesController) promptForTargetBranchNameAndCreatePullRequest(fromBranch *models.Branch, toRemote string) error {
remoteDoesNotExist := lo.NoneBy(self.c.Model().Remotes, func(remote *models.Remote) bool {
return remote.Name == toRemote
})
if remoteDoesNotExist {
return fmt.Errorf(self.c.Tr.NoValidRemoteName, toRemote)
}
self.c.Prompt(types.PromptOpts{
Title: fmt.Sprintf("%s → %s/", fromBranch.UpstreamBranch, toRemote),
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesForRemoteSuggestionsFunc(toRemote),
HandleConfirm: func(toBranch string) error {
self.c.Log.Debugf("PR will target branch '%s' on remote '%s'", toBranch, toRemote)
return self.createPullRequest(fromBranch.UpstreamBranch, toBranch)
},
})
return nil
}
func (self *BranchesController) createPullRequest(from string, to string) error {
url, err := self.c.Helpers().Host.GetPullRequestURL(from, to)
if err != nil {
@@ -837,16 +787,6 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab
return nil
}
func (self *BranchesController) branchesAreReal(selectedBranches []*models.Branch, startIdx int, endIdx int) *types.DisabledReason {
if !lo.EveryBy(selectedBranches, func(branch *models.Branch) bool {
return branch.IsRealBranch()
}) {
return &types.DisabledReason{Text: self.c.Tr.SelectedItemIsNotABranch}
}
return nil
}
func (self *BranchesController) notMergingIntoYourself(branch *models.Branch) *types.DisabledReason {
selectedBranchName := branch.Name
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
+44 -10
View File
@@ -24,20 +24,54 @@ func NewConfirmationController(
}
func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: func() error { return self.context().State.OnConfirm() },
Description: self.c.Tr.Confirm,
DisplayOnScreen: true,
},
{
bindings := []*types.Binding{}
if self.context().State.Multiline {
bindings = append(bindings,
&types.Binding{
// Hard coding because this is always the button that adds a newline.
Key: opts.GetKey("<enter>"),
Handler: func() error {
self.c.Log.Warn("In <enter> handler")
self.c.Views().Confirmation.TextArea.TypeString("\n")
self.c.Views().Confirmation.RenderTextArea()
return nil
},
Description: self.c.Tr.Confirm,
DisplayOnScreen: false,
},
&types.Binding{
Key: opts.GetKey(opts.Config.Universal.ConfirmInEditor),
Handler: func() error {
self.c.Log.Warn("In <alt-enter> handler")
return self.context().State.OnConfirm()
},
Description: self.c.Tr.Confirm,
DisplayOnScreen: true,
},
)
} else {
bindings = append(bindings,
&types.Binding{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Handler: func() error {
self.c.Log.Warn("In single-line confirm handler")
return self.context().State.OnConfirm()
},
Description: self.c.Tr.Confirm,
DisplayOnScreen: true,
},
)
}
bindings = append(bindings,
&types.Binding{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: func() error { return self.context().State.OnClose() },
Description: self.c.Tr.CloseCancel,
DisplayOnScreen: true,
},
{
&types.Binding{
Key: opts.GetKey(opts.Config.Universal.TogglePanel),
Handler: func() error {
if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 {
@@ -54,7 +88,7 @@ func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) [
return nil
},
},
}
)
return bindings
}
+49 -114
View File
@@ -1,7 +1,6 @@
package helpers
import (
"errors"
"strings"
"github.com/jesseduffield/gocui"
@@ -10,7 +9,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo"
)
type BranchesHelper struct {
@@ -25,16 +23,12 @@ func NewBranchesHelper(c *HelperCommon, worktreeHelper *WorktreeHelper) *Branche
}
}
func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error {
if len(branches) > 1 {
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
}
} else if self.checkedOutByOtherWorktree(branches[0]) {
return self.promptWorktreeBranchDelete(branches[0])
func (self *BranchesHelper) ConfirmLocalDelete(branch *models.Branch) error {
if self.checkedOutByOtherWorktree(branch) {
return self.promptWorktreeBranchDelete(branch)
}
allBranchesMerged, err := self.allBranchesMerged(branches)
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
if err != nil {
return err
}
@@ -42,32 +36,24 @@ func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error
doDelete := func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(_ gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
return err
}
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
})
}
if allBranchesMerged {
if isMerged {
return doDelete()
}
title := self.c.Tr.ForceDeleteBranchTitle
var message string
if len(branches) == 1 {
message = utils.ResolvePlaceholderString(
self.c.Tr.ForceDeleteBranchMessage,
map[string]string{
"selectedBranchName": branches[0].Name,
},
)
} else {
message = self.c.Tr.ForceDeleteBranchesMessage
}
message := utils.ResolvePlaceholderString(
self.c.Tr.ForceDeleteBranchMessage,
map[string]string{
"selectedBranchName": branch.Name,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: title,
@@ -80,36 +66,27 @@ func (self *BranchesHelper) ConfirmLocalDelete(branches []*models.Branch) error
return nil
}
func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranches []*models.RemoteBranch) error {
var title string
if len(remoteBranches) == 1 {
title = utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{
"selectedBranchName": remoteBranches[0].Name,
},
)
} else {
title = self.c.Tr.DeleteBranchesTitle
}
var prompt string
if len(remoteBranches) == 1 {
prompt = utils.ResolvePlaceholderString(
self.c.Tr.DeleteRemoteBranchPrompt,
map[string]string{
"selectedBranchName": remoteBranches[0].Name,
"upstream": remoteBranches[0].RemoteName,
},
)
} else {
prompt = self.c.Tr.DeleteRemoteBranchesPrompt
}
func (self *BranchesHelper) ConfirmDeleteRemote(remoteName string, branchName string) error {
title := utils.ResolvePlaceholderString(
self.c.Tr.DeleteBranchTitle,
map[string]string{
"selectedBranchName": branchName,
},
)
prompt := utils.ResolvePlaceholderString(
self.c.Tr.DeleteRemoteBranchPrompt,
map[string]string{
"selectedBranchName": branchName,
"upstream": remoteName,
},
)
self.c.Confirm(types.ConfirmOpts{
Title: title,
Prompt: prompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
if err := self.c.Git().Remote.DeleteRemoteBranch(task, remoteName, branchName); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
@@ -120,41 +97,32 @@ func (self *BranchesHelper) ConfirmDeleteRemote(remoteBranches []*models.RemoteB
return nil
}
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branches []*models.Branch) error {
if lo.SomeBy(branches, func(branch *models.Branch) bool { return self.checkedOutByOtherWorktree(branch) }) {
return errors.New(self.c.Tr.SomeBranchesCheckedOutByWorktreeError)
func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branch *models.Branch) error {
if self.checkedOutByOtherWorktree(branch) {
return self.promptWorktreeBranchDelete(branch)
}
allBranchesMerged, err := self.allBranchesMerged(branches)
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
if err != nil {
return err
}
var prompt string
if len(branches) == 1 {
prompt = utils.ResolvePlaceholderString(
self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
prompt := utils.ResolvePlaceholderString(
self.c.Tr.DeleteLocalAndRemoteBranchPrompt,
map[string]string{
"localBranchName": branch.Name,
"remoteBranchName": branch.UpstreamBranch,
"remoteName": branch.UpstreamRemote,
},
)
if !isMerged {
prompt += "\n\n" + utils.ResolvePlaceholderString(
self.c.Tr.ForceDeleteBranchMessage,
map[string]string{
"localBranchName": branches[0].Name,
"remoteBranchName": branches[0].UpstreamBranch,
"remoteName": branches[0].UpstreamRemote,
"selectedBranchName": branch.Name,
},
)
} else {
prompt = self.c.Tr.DeleteLocalAndRemoteBranchesPrompt
}
if !allBranchesMerged {
if len(branches) == 1 {
prompt += "\n\n" + utils.ResolvePlaceholderString(
self.c.Tr.ForceDeleteBranchMessage,
map[string]string{
"selectedBranchName": branches[0].Name,
},
)
} else {
prompt += "\n\n" + self.c.Tr.ForceDeleteBranchesMessage
}
}
self.c.Confirm(types.ConfirmOpts{
@@ -162,24 +130,18 @@ func (self *BranchesHelper) ConfirmLocalAndRemoteDelete(branches []*models.Branc
Prompt: prompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(task gocui.Task) error {
// Delete the remote branches first so that we keep the local ones
// Delete the remote branch first so that we keep the local one
// in case of failure
remoteBranches := lo.Map(branches, func(branch *models.Branch, _ int) *models.RemoteBranch {
return &models.RemoteBranch{Name: branch.UpstreamBranch, RemoteName: branch.UpstreamRemote}
})
if err := self.deleteRemoteBranches(remoteBranches, task); err != nil {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
if err := self.c.Git().Remote.DeleteRemoteBranch(task, branch.UpstreamRemote, branch.Name); err != nil {
return err
}
self.c.LogAction(self.c.Tr.Actions.DeleteLocalBranch)
branchNames := lo.Map(branches, func(branch *models.Branch, _ int) string { return branch.Name })
if err := self.c.Git().Branch.LocalDelete(branchNames, true); err != nil {
if err := self.c.Git().Branch.LocalDelete(branch.Name, true); err != nil {
return err
}
selectionStart, _ := self.c.Contexts().Branches.GetSelectionRange()
self.c.Contexts().Branches.SetSelectedLineIdx(selectionStart)
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}})
})
},
@@ -236,30 +198,3 @@ func (self *BranchesHelper) promptWorktreeBranchDelete(selectedBranch *models.Br
},
})
}
func (self *BranchesHelper) allBranchesMerged(branches []*models.Branch) (bool, error) {
allBranchesMerged := true
for _, branch := range branches {
isMerged, err := self.c.Git().Branch.IsBranchMerged(branch, self.c.Model().MainBranches)
if err != nil {
return false, err
}
if !isMerged {
allBranchesMerged = false
break
}
}
return allBranchesMerged, nil
}
func (self *BranchesHelper) deleteRemoteBranches(remoteBranches []*models.RemoteBranch, task gocui.Task) error {
remotes := lo.GroupBy(remoteBranches, func(branch *models.RemoteBranch) string { return branch.RemoteName })
for remote, branches := range remotes {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch)
branchNames := lo.Map(branches, func(branch *models.RemoteBranch, _ int) string { return branch.Name })
if err := self.c.Git().Remote.DeleteRemoteBranch(task, remote, branchNames); err != nil {
return err
}
}
return nil
}
@@ -143,7 +143,6 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp
opts.SummaryTitle,
opts.DescriptionTitle,
opts.PreserveMessage,
opts.InitialMessage,
onConfirm,
opts.OnSwitchToEditor,
)
@@ -178,9 +177,8 @@ func (self *CommitsHelper) HandleCommitConfirm() error {
func (self *CommitsHelper) CloseCommitMessagePanel() {
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
message := self.JoinCommitMessageAndUnwrappedDescription()
if message != self.c.Contexts().CommitMessage.GetInitialMessage() {
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
}
self.c.Contexts().CommitMessage.SetPreservedMessage(message)
} else {
self.SetMessageAndDescriptionInView("")
}
@@ -3,11 +3,13 @@ package helpers
import (
goContext "context"
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
)
type ConfirmationHelper struct {
@@ -56,9 +58,61 @@ func (self *ConfirmationHelper) DeactivateConfirmationPrompt() {
self.clearConfirmationViewKeyBindings()
}
// Temporary hack: we're just duplicating the logic in `gocui.lineWrap`
func getMessageHeight(wrap bool, message string, width int) int {
wrappedLines, _, _ := utils.WrapViewLinesToWidth(wrap, message, width)
return len(wrappedLines)
return len(wrapMessageToWidth(wrap, message, width))
}
func wrapMessageToWidth(wrap bool, message string, width int) []string {
lines := strings.Split(message, "\n")
if !wrap {
return lines
}
wrappedLines := make([]string, 0, len(lines))
for _, line := range lines {
n := 0
offset := 0
lastWhitespaceIndex := -1
for i, currChr := range line {
rw := runewidth.RuneWidth(currChr)
n += rw
if n > width {
if currChr == ' ' {
wrappedLines = append(wrappedLines, line[offset:i])
offset = i + 1
n = 0
} else if currChr == '-' {
wrappedLines = append(wrappedLines, line[offset:i])
offset = i
n = rw
} else if lastWhitespaceIndex != -1 && lastWhitespaceIndex+1 != i {
if line[lastWhitespaceIndex] == '-' {
wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex+1])
offset = lastWhitespaceIndex + 1
n = i - lastWhitespaceIndex
} else {
wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex])
offset = lastWhitespaceIndex + 1
n = i - lastWhitespaceIndex + 1
}
} else {
wrappedLines = append(wrappedLines, line[offset:i])
offset = i
n = rw
}
lastWhitespaceIndex = -1
} else if currChr == ' ' || currChr == '-' {
lastWhitespaceIndex = i
}
}
wrappedLines = append(wrappedLines, line[offset:])
}
return wrappedLines
}
func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int, parentPopupContext types.Context) (int, int, int, int) {
@@ -103,12 +157,14 @@ func (self *ConfirmationHelper) getPopupPanelWidth() int {
func (self *ConfirmationHelper) prepareConfirmationPanel(
opts types.ConfirmOpts,
) {
self.c.Views().Confirmation.Title = opts.Title
self.c.Contexts().Confirmation.State.Multiline = opts.Multiline
confirmationView := self.c.Views().Confirmation
confirmationView.Title = opts.Title
// for now we do not support wrapping in our editor
self.c.Views().Confirmation.Wrap = !opts.Editable
self.c.Views().Confirmation.FgColor = theme.GocuiDefaultTextColor
self.c.Views().Confirmation.Mask = runeForMask(opts.Mask)
self.c.Views().Confirmation.SetOrigin(0, 0)
confirmationView.Wrap = !opts.Editable
confirmationView.FgColor = theme.GocuiDefaultTextColor
confirmationView.Mask = runeForMask(opts.Mask)
confirmationView.SetOrigin(0, 0)
suggestionsContext := self.c.Contexts().Suggestions
suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc
@@ -153,6 +209,7 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
self.prepareConfirmationPanel(
types.ConfirmOpts{
Title: opts.Title,
Multiline: opts.Multiline,
Prompt: opts.Prompt,
FindSuggestionsFunc: opts.FindSuggestionsFunc,
Editable: opts.Editable,
@@ -160,6 +217,14 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
})
confirmationView := self.c.Views().Confirmation
confirmationView.Editable = opts.Editable
if opts.Multiline {
confirmationView.Subtitle = utils.ResolvePlaceholderString(
self.c.Tr.PressToSubmit,
map[string]string{"Key": self.c.UserConfig().Keybinding.Universal.ConfirmInEditor},
)
} else {
confirmationView.Subtitle = ""
}
if opts.Editable {
textArea := confirmationView.TextArea
@@ -276,7 +341,7 @@ func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int {
var promptLines []string
prompt := self.c.Contexts().Menu.GetPrompt()
if len(prompt) > 0 {
promptLines, _, _ = utils.WrapViewLinesToWidth(true, prompt, contentWidth)
promptLines = wrapMessageToWidth(true, prompt, contentWidth)
promptLines = append(promptLines, "")
}
self.c.Contexts().Menu.SetPromptLines(promptLines)
@@ -304,14 +369,22 @@ func (self *ConfirmationHelper) resizeConfirmationPanel(parentPopupContext types
suggestionsViewHeight = 11
}
panelWidth := self.getPopupPanelWidth()
contentWidth := panelWidth - 2 // minus 2 for the frame
prompt := self.c.Views().Confirmation.Buffer()
wrap := true
if self.c.Views().Confirmation.Editable {
prompt = self.c.Views().Confirmation.TextArea.GetContent()
wrap = false
}
panelHeight := getMessageHeight(wrap, prompt, contentWidth) + suggestionsViewHeight
multiline := self.c.Contexts().Confirmation.State.Multiline
inputHeight := getMessageHeight(wrap, prompt, panelWidth)
minMultilineHeight := 5
if multiline && inputHeight < minMultilineHeight {
inputHeight = minMultilineHeight
}
panelHeight := inputHeight + suggestionsViewHeight
x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight, parentPopupContext)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
@@ -34,6 +34,10 @@ func (self *DiffHelper) DiffArgs() []string {
output = append(output, "-R")
}
if self.c.GetAppState().IgnoreWhitespaceInDiffView {
output = append(output, "--ignore-all-space")
}
output = append(output, "--")
file := self.currentlySelectedFilename()
@@ -55,6 +59,9 @@ func (self *DiffHelper) GetUpdateTaskForRenderingCommitsDiff(commit *models.Comm
if refRange != nil {
from, to := refRange.From, refRange.To
args := []string{from.ParentRefName(), to.RefName(), "--stat", "-p"}
if self.c.GetAppState().IgnoreWhitespaceInDiffView {
args = append(args, "--ignore-all-space")
}
args = append(args, "--")
if path := self.c.Modes().Filtering.GetPath(); path != "" {
args = append(args, path)
@@ -91,7 +91,7 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt
oldState := context.GetState()
state := patch_exploring.NewState(diff, selectedLineIdx, context.GetView(), oldState)
state := patch_exploring.NewState(diff, selectedLineIdx, oldState, self.c.Log)
context.SetState(state)
if state == nil {
self.Escape()
+1 -1
View File
@@ -23,7 +23,7 @@ func NewSnakeHelper(c *HelperCommon) *SnakeHelper {
func (self *SnakeHelper) StartGame() {
view := self.c.Views().Snake
game := snake.NewGame(view.InnerWidth(), view.InnerHeight(), self.renderSnakeGame, self.c.LogAction)
game := snake.NewGame(view.Width(), view.Height(), self.renderSnakeGame, self.c.LogAction)
self.game = game
game.Start()
}
@@ -63,11 +63,11 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) {
secondaryContext.GetMutex().Lock()
mainContext.SetState(
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetView(), mainContext.GetState()),
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), self.c.Log),
)
secondaryContext.SetState(
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetView(), secondaryContext.GetState()),
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), self.c.Log),
)
mainState := mainContext.GetState()
@@ -162,26 +162,10 @@ func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string {
})
}
func (self *SuggestionsHelper) getRemoteBranchNamesForRemote(remoteName string) []string {
remote, ok := lo.Find(self.c.Model().Remotes, func(remote *models.Remote) bool {
return remote.Name == remoteName
})
if ok {
return lo.Map(remote.Branches, func(branch *models.RemoteBranch, _ int) string {
return branch.Name
})
}
return nil
}
func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return FilterFunc(self.getRemoteBranchNames(separator), self.c.UserConfig().Gui.UseFuzzySearch())
}
func (self *SuggestionsHelper) GetRemoteBranchesForRemoteSuggestionsFunc(remoteName string) func(string) []*types.Suggestion {
return FilterFunc(self.getRemoteBranchNamesForRemote(remoteName), self.c.UserConfig().Gui.UseFuzzySearch())
}
func (self *SuggestionsHelper) getTagNames() []string {
return lo.Map(self.c.Model().Tags, func(tag *models.Tag, _ int) string {
return tag.Name
+20 -77
View File
@@ -9,7 +9,6 @@ import (
"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/context/traits"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/style"
@@ -116,7 +115,7 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
},
{
Key: opts.GetKey(editCommitKey),
Handler: self.withItemsRange(self.edit),
Handler: self.withItems(self.edit),
GetDisabledReason: self.require(
self.itemRangeSelected(self.midRebaseCommandEnabled),
),
@@ -497,17 +496,12 @@ func (self *LocalCommitsController) drop(selectedCommits []*models.Commit, start
return self.updateTodos(todo.Drop, selectedCommits)
}
isMerge := selectedCommits[0].IsMerge()
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.DropCommitTitle,
Prompt: lo.Ternary(isMerge, self.c.Tr.DropMergeCommitPrompt, self.c.Tr.DropCommitPrompt),
Prompt: self.c.Tr.DropCommitPrompt,
HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DroppingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.DropCommit)
if isMerge {
return self.dropMergeCommit(startIdx)
}
return self.interactiveRebase(todo.Drop, startIdx, endIdx)
})
},
@@ -516,30 +510,11 @@ func (self *LocalCommitsController) drop(selectedCommits []*models.Commit, start
return nil
}
func (self *LocalCommitsController) dropMergeCommit(commitIdx int) error {
err := self.c.Git().Rebase.DropMergeCommit(self.c.Model().Commits, commitIdx)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
}
func (self *LocalCommitsController) edit(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
func (self *LocalCommitsController) edit(selectedCommits []*models.Commit) error {
if self.isRebasing() {
return self.updateTodos(todo.Edit, selectedCommits)
}
commits := self.c.Model().Commits
if !commits[endIdx].IsMerge() {
selectionRangeAndMode := self.getSelectionRangeAndMode()
err := self.c.Git().Rebase.InteractiveRebase(commits, startIdx, endIdx, todo.Edit)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err,
types.RefreshOptions{
Mode: types.BLOCK_UI, Then: func() error {
self.restoreSelectionRangeAndMode(selectionRangeAndMode)
return nil
},
})
}
return self.startInteractiveRebaseWithEdit(selectedCommits)
}
@@ -557,7 +532,10 @@ func (self *LocalCommitsController) startInteractiveRebaseWithEdit(
) error {
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.EditCommit)
selectionRangeAndMode := self.getSelectionRangeAndMode()
selectedIdx, rangeStartIdx, rangeSelectMode := self.context().GetSelectionRangeAndMode()
commits := self.c.Model().Commits
selectedHash := commits[selectedIdx].Hash
rangeStartHash := commits[rangeStartIdx].Hash
err := self.c.Git().Rebase.EditRebase(commitsToEdit[len(commitsToEdit)-1].Hash)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err,
@@ -576,41 +554,23 @@ func (self *LocalCommitsController) startInteractiveRebaseWithEdit(
}
}
self.restoreSelectionRangeAndMode(selectionRangeAndMode)
// We need to select the same commit range again because after starting a rebase,
// new lines can be added for update-ref commands in the TODO file, due to
// stacked branches. So the selected commits may be in different positions in the list.
_, newSelectedIdx, ok1 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == selectedHash
})
_, newRangeStartIdx, ok2 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == rangeStartHash
})
if ok1 && ok2 {
self.context().SetSelectionRangeAndMode(newSelectedIdx, newRangeStartIdx, rangeSelectMode)
}
return nil
}})
})
}
type SelectionRangeAndMode struct {
selectedHash string
rangeStartHash string
mode traits.RangeSelectMode
}
func (self *LocalCommitsController) getSelectionRangeAndMode() SelectionRangeAndMode {
selectedIdx, rangeStartIdx, rangeSelectMode := self.context().GetSelectionRangeAndMode()
commits := self.c.Model().Commits
selectedHash := commits[selectedIdx].Hash
rangeStartHash := commits[rangeStartIdx].Hash
return SelectionRangeAndMode{selectedHash, rangeStartHash, rangeSelectMode}
}
func (self *LocalCommitsController) restoreSelectionRangeAndMode(selectionRangeAndMode SelectionRangeAndMode) {
// We need to select the same commit range again because after starting a rebase,
// new lines can be added for update-ref commands in the TODO file, due to
// stacked branches. So the selected commits may be in different positions in the list.
_, newSelectedIdx, ok1 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == selectionRangeAndMode.selectedHash
})
_, newRangeStartIdx, ok2 := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.Hash == selectionRangeAndMode.rangeStartHash
})
if ok1 && ok2 {
self.context().SetSelectionRangeAndMode(newSelectedIdx, newRangeStartIdx, selectionRangeAndMode.mode)
}
}
func (self *LocalCommitsController) findCommitForQuickStartInteractiveRebase() (*models.Commit, error) {
commit, index, ok := lo.FindIndexOf(self.c.Model().Commits, func(c *models.Commit) bool {
return c.IsMerge() || c.Status == models.StatusMerged
@@ -1282,11 +1242,6 @@ func (self *LocalCommitsController) handleOpenLogMenu() error {
OnPress: onPress("author-date-order"),
Widget: types.MakeMenuRadioButton(currentValue == "author-date-order"),
},
{
Label: "default",
OnPress: onPress("default"),
Widget: types.MakeMenuRadioButton(currentValue == "default"),
},
},
})
},
@@ -1368,15 +1323,11 @@ func (self *LocalCommitsController) canFindCommitForSquashFixupsInCurrentBranch(
return nil
}
func (self *LocalCommitsController) canSquashOrFixup(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
func (self *LocalCommitsController) canSquashOrFixup(_selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if endIdx >= len(self.c.Model().Commits)-1 {
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupFirstCommit}
}
if lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.CannotSquashOrFixupMergeCommit}
}
return nil
}
@@ -1434,10 +1385,6 @@ func (self *LocalCommitsController) midRebaseCommandEnabled(selectedCommits []*m
// Ensures that if we are mid-rebase, we're only selecting commits that can be moved
func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if !self.isRebasing() {
if lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.CannotMoveMergeCommit}
}
return nil
}
@@ -1458,10 +1405,6 @@ func (self *LocalCommitsController) midRebaseMoveCommandEnabled(selectedCommits
func (self *LocalCommitsController) canDropCommits(selectedCommits []*models.Commit, startIdx int, endIdx int) *types.DisabledReason {
if !self.isRebasing() {
if len(selectedCommits) > 1 && lo.SomeBy(selectedCommits, func(c *models.Commit) bool { return c.IsMerge() }) {
return &types.DisabledReason{Text: self.c.Tr.DroppingMergeRequiresSingleSelection}
}
return nil
}
@@ -65,7 +65,7 @@ func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsO
func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) {
return func(opts types.OnFocusOpts) {
// no need to change wrap on the secondary view because it can't be interacted with
self.c.Views().PatchBuilding.Wrap = self.c.UserConfig().Gui.WrapLinesInStagingView
self.c.Views().PatchBuilding.Wrap = false
self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts)
}
@@ -73,8 +73,6 @@ func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) {
func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) {
return func(opts types.OnFocusLostOpts) {
self.context().SetState(nil)
self.c.Views().PatchBuilding.Wrap = true
if self.c.Git().Patch.PatchBuilder.IsEmpty() {
@@ -136,13 +134,13 @@ func (self *PatchBuildingController) toggleSelection() error {
if err != nil {
return err
}
currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedPatchLineIdx())
currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedLineIdx())
if currentLineIsStaged {
toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
}
// add range of lines to those set for the file
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
firstLineIdx, lastLineIdx := state.SelectedRange()
if err := toggleFunc(filename, firstLineIdx, lastLineIdx); err != nil {
// might actually want to return an error here
@@ -170,9 +170,9 @@ func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsO
}
func (self *PatchExplorerController) HandlePrevLine() error {
before := self.context.GetState().GetSelectedViewLineIdx()
before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(false)
after := self.context.GetState().GetSelectedViewLineIdx()
after := self.context.GetState().GetSelectedLineIdx()
if self.context.GetState().SelectingLine() {
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
@@ -182,9 +182,9 @@ func (self *PatchExplorerController) HandlePrevLine() error {
}
func (self *PatchExplorerController) HandleNextLine() error {
before := self.context.GetState().GetSelectedViewLineIdx()
before := self.context.GetState().GetSelectedLineIdx()
self.context.GetState().CycleSelection(true)
after := self.context.GetState().GetSelectedViewLineIdx()
after := self.context.GetState().GetSelectedLineIdx()
if self.context.GetState().SelectingLine() {
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
@@ -66,8 +66,8 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.withItems(self.delete),
GetDisabledReason: self.require(self.itemRangeSelected()),
Handler: self.withItem(self.delete),
GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.Delete,
Tooltip: self.c.Tr.DeleteRemoteBranchTooltip,
DisplayOnScreen: true,
@@ -132,8 +132,8 @@ func (self *RemoteBranchesController) context() *context.RemoteBranchesContext {
return self.c.Contexts().RemoteBranches
}
func (self *RemoteBranchesController) delete(selectedBranches []*models.RemoteBranch) error {
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranches)
func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error {
return self.c.Helpers().BranchesHelper.ConfirmDeleteRemote(selectedBranch.RemoteName, selectedBranch.Name)
}
func (self *RemoteBranchesController) merge(selectedBranch *models.RemoteBranch) error {
+5 -6
View File
@@ -118,9 +118,8 @@ func (self *StagingController) GetMouseKeybindings(opts types.KeybindingsOpts) [
func (self *StagingController) GetOnFocus() func(types.OnFocusOpts) {
return func(opts types.OnFocusOpts) {
wrap := self.c.UserConfig().Gui.WrapLinesInStagingView
self.c.Views().Staging.Wrap = wrap
self.c.Views().StagingSecondary.Wrap = wrap
self.c.Views().Staging.Wrap = false
self.c.Views().StagingSecondary.Wrap = false
self.c.Helpers().Staging.RefreshStagingPanel(opts)
}
@@ -221,7 +220,7 @@ func (self *StagingController) applySelection(reverse bool) error {
return nil
}
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
firstLineIdx, lastLineIdx := state.SelectedRange()
patchToApply := patch.
Parse(state.GetDiff()).
Transform(patch.TransformOpts{
@@ -250,7 +249,7 @@ func (self *StagingController) applySelection(reverse bool) error {
}
if state.SelectingRange() {
firstLine, _ := state.SelectedViewRange()
firstLine, _ := state.SelectedRange()
state.SelectLine(firstLine)
}
@@ -291,7 +290,7 @@ func (self *StagingController) editHunk() error {
}
lineOffset := 3
lineIdxInHunk := state.GetSelectedPatchLineIdx() - hunkStartIdx
lineIdxInHunk := state.GetSelectedLineIdx() - hunkStartIdx
if err := self.c.Helpers().Files.EditFileAtLineAndWait(patchFilepath, lineIdxInHunk+lineOffset); err != nil {
return err
}
@@ -169,7 +169,7 @@ func (self *FilesController) animateExplosion() {
// Animates an explosion within the view by drawing a bunch of flamey characters
func (self *FilesController) Explode(v *gocui.View, onDone func()) {
width := v.InnerWidth()
height := v.InnerHeight()
height := v.InnerHeight() + 1
styles := []style.TextStyle{
style.FgLightWhite.SetBold(),
style.FgYellow.SetBold(),
+2
View File
@@ -108,6 +108,8 @@ type Gui struct {
PopupHandler types.IPopupHandler
IsNewRepo bool
IsRefreshingFiles bool
// we use this to decide whether we'll return to the original directory that
+1 -1
View File
@@ -30,7 +30,7 @@ func (gui *Gui) handleInfoClick() error {
view := gui.Views.Information
cx, _ := view.Cursor()
width := view.Width()
width, _ := view.Size()
if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok {
if width-cx > utils.StringWidth(gui.c.Tr.ResetInParentheses) {
+12 -6
View File
@@ -193,6 +193,18 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Tooltip: self.c.Tr.OpenCommandLogMenuTooltip,
OpensMenu: true,
},
{
ViewName: "secondary",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: self.scrollUpSecondary,
},
{
ViewName: "secondary",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: self.scrollDownSecondary,
},
{
ViewName: "main",
Key: gocui.MouseWheelDown,
@@ -207,12 +219,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Description: self.c.Tr.ScrollUp,
Alternative: "fn+down",
},
{
ViewName: "secondary",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: self.scrollDownSecondary,
},
{
ViewName: "secondary",
Key: gocui.MouseWheelUp,
+8 -6
View File
@@ -30,8 +30,8 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// reading more lines into main view buffers upon resize
prevMainView := gui.Views.Main
if prevMainView != nil {
prevMainHeight := prevMainView.Height()
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 + 1
_, prevMainHeight := prevMainView.Size()
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
heightDiff := newMainHeight - prevMainHeight
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
@@ -87,15 +87,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
}
if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES {
oldWidth := view.Width()
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 1
// view.Width() returns the width -1 for some reason
oldWidth := view.Width() + 1
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
if oldWidth != newWidth {
mustRerender = true
}
}
if context.NeedsRerenderOnHeightChange() {
oldHeight := view.Height()
newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 1
// view.Height() returns the height -1 for some reason
oldHeight := view.Height() + 1
newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
if oldHeight != newHeight {
mustRerender = true
}
+1 -1
View File
@@ -108,7 +108,7 @@ func (self *OptionsMapMgr) renderContextOptionsMap() {
}
func (self *OptionsMapMgr) formatBindingInfos(bindingInfos []bindingInfo) string {
width := self.c.Views().Options.InnerWidth() - 2 // -2 for some padding
width := self.c.Views().Options.Width() - 4 // -4 for the padding
var builder strings.Builder
ellipsis := "…"
separator := " | "
+3 -3
View File
@@ -11,8 +11,8 @@ func calculateOrigin(currentOrigin int, bufferHeight int, numLines int, firstLin
// is as close to being in view as possible.
func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, numLines int, needToSeeIdx int, wantToSeeIdx int) int {
origin := currentOrigin
if needToSeeIdx < currentOrigin || needToSeeIdx >= currentOrigin+bufferHeight {
origin = max(min(needToSeeIdx-bufferHeight/2, numLines-bufferHeight), 0)
if needToSeeIdx < currentOrigin || needToSeeIdx > currentOrigin+bufferHeight {
origin = max(min(needToSeeIdx-bufferHeight/2, numLines-bufferHeight-1), 0)
}
bottom := origin + bufferHeight
@@ -21,7 +21,7 @@ func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight in
requiredChange := origin - wantToSeeIdx
allowedChange := bottom - needToSeeIdx
return origin - min(requiredChange, allowedChange)
} else if wantToSeeIdx >= bottom {
} else if wantToSeeIdx > origin+bufferHeight {
requiredChange := wantToSeeIdx - bottom
allowedChange := needToSeeIdx - origin
return origin + min(requiredChange, allowedChange)
+1 -1
View File
@@ -62,7 +62,7 @@ func TestNewOrigin(t *testing.T) {
lastLineIdx: 199,
selectedLineIdx: 199,
selectMode: LINE,
expected: 100,
expected: 99,
},
{
name: "selection within scroll window",
+23 -65
View File
@@ -1,19 +1,15 @@
package patch_exploring
import (
"strings"
"github.com/jesseduffield/generics/set"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// State represents the current state of the patch explorer context i.e. when
// you're staging a file or you're building a patch from an existing commit
// this struct holds the info about the diff you're interacting with and what's currently selected.
type State struct {
// These are in terms of view lines (wrapped), not patch lines
selectedLineIdx int
rangeStartLineIdx int
// If a range is sticky, it means we expand the range when we move up or down.
@@ -22,11 +18,6 @@ type State struct {
diff string
patch *patch.Patch
selectMode selectMode
// Array of indices of the wrapped lines indexed by a patch line index
viewLineIndices []int
// Array of indices of the original patch lines indexed by a wrapped view line index
patchLineIndices []int
}
// these represent what select mode we're in
@@ -38,7 +29,7 @@ const (
HUNK
)
func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *State) *State {
func NewState(diff string, selectedLineIdx int, oldState *State, log *logrus.Entry) *State {
if oldState != nil && diff == oldState.diff && selectedLineIdx == -1 {
// if we're here then we can return the old state. If selectedLineIdx was not -1
// then that would mean we were trying to click and potentiall drag a range, which
@@ -52,8 +43,6 @@ func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *Stat
return nil
}
viewLineIndices, patchLineIndices := wrapPatchLines(diff, view)
rangeStartLineIdx := 0
if oldState != nil {
rangeStartLineIdx = oldState.rangeStartLineIdx
@@ -62,10 +51,6 @@ func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *Stat
selectMode := LINE
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
if selectedLineIdx >= 0 {
// Clamp to the number of wrapped view lines; index might be out of
// bounds if a custom pager is being used which produces more lines
selectedLineIdx = min(selectedLineIdx, len(viewLineIndices)-1)
selectMode = RANGE
rangeStartLineIdx = selectedLineIdx
} else if oldState != nil {
@@ -73,9 +58,9 @@ func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *Stat
if oldState.selectMode == HUNK {
selectMode = HUNK
}
selectedLineIdx = viewLineIndices[patch.GetNextChangeIdx(oldState.patchLineIndices[oldState.selectedLineIdx])]
selectedLineIdx = patch.GetNextChangeIdx(oldState.selectedLineIdx)
} else {
selectedLineIdx = viewLineIndices[patch.GetNextChangeIdx(0)]
selectedLineIdx = patch.GetNextChangeIdx(0)
}
return &State{
@@ -85,33 +70,10 @@ func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *Stat
rangeStartLineIdx: rangeStartLineIdx,
rangeIsSticky: false,
diff: diff,
viewLineIndices: viewLineIndices,
patchLineIndices: patchLineIndices,
}
}
func (s *State) OnViewWidthChanged(view *gocui.View) {
if !view.Wrap {
return
}
selectedPatchLineIdx := s.patchLineIndices[s.selectedLineIdx]
var rangeStartPatchLineIdx int
if s.selectMode == RANGE {
rangeStartPatchLineIdx = s.patchLineIndices[s.rangeStartLineIdx]
}
s.viewLineIndices, s.patchLineIndices = wrapPatchLines(s.diff, view)
s.selectedLineIdx = s.viewLineIndices[selectedPatchLineIdx]
if s.selectMode == RANGE {
s.rangeStartLineIdx = s.viewLineIndices[rangeStartPatchLineIdx]
}
}
func (s *State) GetSelectedPatchLineIdx() int {
return s.patchLineIndices[s.selectedLineIdx]
}
func (s *State) GetSelectedViewLineIdx() int {
func (s *State) GetSelectedLineIdx() int {
return s.selectedLineIdx
}
@@ -181,8 +143,8 @@ func (s *State) SelectLine(newSelectedLineIdx int) {
func (s *State) selectLineWithoutRangeCheck(newSelectedLineIdx int) {
if newSelectedLineIdx < 0 {
newSelectedLineIdx = 0
} else if newSelectedLineIdx > len(s.patchLineIndices)-1 {
newSelectedLineIdx = len(s.patchLineIndices) - 1
} else if newSelectedLineIdx > s.patch.LineCount()-1 {
newSelectedLineIdx = s.patch.LineCount() - 1
}
s.selectedLineIdx = newSelectedLineIdx
@@ -216,12 +178,12 @@ func (s *State) CycleHunk(forward bool) {
change = -1
}
hunkIdx := s.patch.HunkContainingLine(s.patchLineIndices[s.selectedLineIdx])
hunkIdx := s.patch.HunkContainingLine(s.selectedLineIdx)
if hunkIdx != -1 {
newHunkIdx := hunkIdx + change
if newHunkIdx >= 0 && newHunkIdx < s.patch.HunkCount() {
start := s.patch.HunkStartIdx(newHunkIdx)
s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(start)]
s.selectedLineIdx = s.patch.GetNextChangeIdx(start)
}
}
}
@@ -254,17 +216,16 @@ func (s *State) CycleRange(forward bool) {
// returns first and last patch line index of current hunk
func (s *State) CurrentHunkBounds() (int, int) {
hunkIdx := s.patch.HunkContainingLine(s.patchLineIndices[s.selectedLineIdx])
hunkIdx := s.patch.HunkContainingLine(s.selectedLineIdx)
start := s.patch.HunkStartIdx(hunkIdx)
end := s.patch.HunkEndIdx(hunkIdx)
return start, end
}
func (s *State) SelectedViewRange() (int, int) {
func (s *State) SelectedRange() (int, int) {
switch s.selectMode {
case HUNK:
start, end := s.CurrentHunkBounds()
return s.viewLineIndices[start], s.viewLineIndices[end]
return s.CurrentHunkBounds()
case RANGE:
if s.rangeStartLineIdx > s.selectedLineIdx {
return s.selectedLineIdx, s.rangeStartLineIdx
@@ -279,13 +240,8 @@ func (s *State) SelectedViewRange() (int, int) {
}
}
func (s *State) SelectedPatchRange() (int, int) {
start, end := s.SelectedViewRange()
return s.patchLineIndices[start], s.patchLineIndices[end]
}
func (s *State) CurrentLineNumber() int {
return s.patch.LineNumberOfLine(s.patchLineIndices[s.selectedLineIdx])
return s.patch.LineNumberOfLine(s.selectedLineIdx)
}
func (s *State) AdjustSelectedLineIdx(change int) {
@@ -301,13 +257,13 @@ func (s *State) RenderForLineIndices(includedLineIndices []int) string {
}
func (s *State) PlainRenderSelected() string {
firstLineIdx, lastLineIdx := s.SelectedPatchRange()
firstLineIdx, lastLineIdx := s.SelectedRange()
return s.patch.FormatRangePlain(firstLineIdx, lastLineIdx)
}
func (s *State) SelectBottom() {
s.DismissHunkSelectMode()
s.SelectLine(len(s.patchLineIndices) - 1)
s.SelectLine(s.patch.LineCount() - 1)
}
func (s *State) SelectTop() {
@@ -316,13 +272,15 @@ func (s *State) SelectTop() {
}
func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int, numLines int) int {
firstLineIdx, lastLineIdx := s.SelectedViewRange()
firstLineIdx, lastLineIdx := s.SelectedRange()
return calculateOrigin(currentOrigin, bufferHeight, numLines, firstLineIdx, lastLineIdx, s.GetSelectedViewLineIdx(), s.selectMode)
return calculateOrigin(currentOrigin, bufferHeight, numLines, firstLineIdx, lastLineIdx, s.GetSelectedLineIdx(), s.selectMode)
}
func wrapPatchLines(diff string, view *gocui.View) ([]int, []int) {
_, viewLineIndices, patchLineIndices := utils.WrapViewLinesToWidth(
view.Wrap, strings.TrimSuffix(diff, "\n"), view.InnerWidth())
return viewLineIndices, patchLineIndices
func (s *State) RangeStartLineIdx() (int, bool) {
if s.selectMode == RANGE {
return s.rangeStartLineIdx, true
}
return 0, false
}
+1
View File
@@ -107,6 +107,7 @@ func (self *PopupHandler) Confirm(opts types.ConfirmOpts) {
func (self *PopupHandler) Prompt(opts types.PromptOpts) {
self.createPopupPanelFn(context.Background(), types.CreatePopupPanelOpts{
Title: opts.Title,
Multiline: opts.Multiline,
Prompt: opts.InitialContent,
Editable: true,
HandleConfirmPrompt: opts.HandleConfirm,
+1 -26
View File
@@ -22,13 +22,12 @@ func RenderFileTree(
tree filetree.IFileTree,
submoduleConfigs []*models.SubmoduleConfig,
showFileIcons bool,
showNumstat bool,
) []string {
collapsedPaths := tree.CollapsedPaths()
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
fileNode := filetree.NewFileNode(node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showFileIcons, submoduleConfigs, node)
})
}
@@ -112,7 +111,6 @@ func getFileLine(
hasStagedChanges bool,
treeDepth int,
visualDepth int,
showNumstat,
showFileIcons bool,
submoduleConfigs []*models.SubmoduleConfig,
node *filetree.Node[models.File],
@@ -167,12 +165,6 @@ func getFileLine(
output += theme.DefaultTextColor.Sprint(" (submodule)")
}
if file != nil && showNumstat {
if lineChanges := formatLineChanges(file.LinesAdded, file.LinesDeleted); lineChanges != "" {
output += " " + lineChanges
}
}
return output
}
@@ -194,23 +186,6 @@ func formatFileStatus(file *models.File, restColor style.TextStyle) string {
return firstCharCl.Sprint(firstChar) + secondCharCl.Sprint(secondChar)
}
func formatLineChanges(linesAdded, linesDeleted int) string {
output := ""
if linesAdded != 0 {
output += style.FgGreen.Sprintf("+%d", linesAdded)
}
if linesDeleted != 0 {
if output != "" {
output += " "
}
output += style.FgRed.Sprintf("-%d", linesDeleted)
}
return output
}
func getCommitFileLine(
isCollapsed bool,
treeDepth int,
+6 -23
View File
@@ -19,12 +19,11 @@ func toStringSlice(str string) []string {
func TestRenderFileTree(t *testing.T) {
scenarios := []struct {
name string
root *filetree.FileNode
files []*models.File
collapsedPaths []string
showLineChanges bool
expected []string
name string
root *filetree.FileNode
files []*models.File
collapsedPaths []string
expected []string
}{
{
name: "nil node",
@@ -38,22 +37,6 @@ func TestRenderFileTree(t *testing.T) {
},
expected: []string{" M test"},
},
{
name: "numstat",
files: []*models.File{
{Name: "test", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 1, LinesDeleted: 1},
{Name: "test2", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 1},
{Name: "test3", ShortStatus: " M", HasStagedChanges: true, LinesDeleted: 1},
{Name: "test4", ShortStatus: " M", HasStagedChanges: true, LinesAdded: 0, LinesDeleted: 0},
},
showLineChanges: true,
expected: []string{
" M test +1 -1",
" M test2 +1",
" M test3 -1",
" M test4",
},
},
{
name: "big example",
files: []*models.File{
@@ -89,7 +72,7 @@ M file1
for _, path := range s.collapsedPaths {
viewModel.ToggleCollapsed(path)
}
result := RenderFileTree(viewModel, nil, false, s.showLineChanges)
result := RenderFileTree(viewModel, nil, false)
assert.EqualValues(t, s.expected, result)
})
}
+2 -2
View File
@@ -16,7 +16,7 @@ import (
)
func (gui *Gui) desiredPtySize(view *gocui.View) *pty.Winsize {
width, height := view.InnerSize()
width, height := view.Size()
return &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}
}
@@ -45,7 +45,7 @@ func (gui *Gui) onResize() error {
// pseudo-terminal meaning we'll get the behaviour we want from the underlying
// command.
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
width := view.InnerWidth()
width, _ := view.Size()
pager := gui.git.Config.GetPager(width)
externalDiffCommand := gui.Config.GetUserConfig().Git.Paging.ExternalDiffCommand
+7 -3
View File
@@ -21,7 +21,8 @@ func (gui *Gui) updateRecentRepoList() error {
if err != nil {
return err
}
recentRepos = newRecentReposList(recentRepos, currentRepo)
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.IsNewRepo = known
// TODO: migrate this file to use forward slashes on all OSes for consistency
// (windows uses backslashes at the moment)
gui.c.GetAppState().RecentRepos = recentRepos
@@ -29,7 +30,8 @@ func (gui *Gui) updateRecentRepoList() error {
}
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet
func newRecentReposList(recentRepos []string, currentRepo string) []string {
func newRecentReposList(recentRepos []string, currentRepo string) (bool, []string) {
isNew := true
newRepos := []string{currentRepo}
for _, repo := range recentRepos {
if repo != currentRepo {
@@ -37,7 +39,9 @@ func newRecentReposList(recentRepos []string, currentRepo string) []string {
continue
}
newRepos = append(newRepos, repo)
} else {
isNew = false
}
}
return newRepos
return isNew, newRepos
}
@@ -79,6 +79,14 @@ func (self *HandlerCreator) call(customCommand config.CustomCommand) func() erro
}
return self.inputPrompt(resolvedPrompt, wrappedF)
}
case "textbox":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
if err != nil {
return err
}
return self.textboxPrompt(resolvedPrompt, wrappedF)
}
case "menu":
f = func() error {
resolvedPrompt, err := self.resolver.resolvePrompt(&prompt, resolveTemplate)
@@ -130,6 +138,19 @@ func (self *HandlerCreator) inputPrompt(prompt *config.CustomCommandPrompt, wrap
return nil
}
func (self *HandlerCreator) textboxPrompt(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error {
self.c.Prompt(types.PromptOpts{
Title: prompt.Title,
InitialContent: prompt.InitialValue,
Multiline: true,
HandleConfirm: func(str string) error {
return wrappedF(str)
},
})
return nil
}
func (self *HandlerCreator) generateFindSuggestionsFunc(prompt *config.CustomCommandPrompt) (func(string) []*types.Suggestion, error) {
if prompt.Suggestions.Preset != "" && prompt.Suggestions.Command != "" {
return nil, fmt.Errorf(
+3
View File
@@ -153,6 +153,7 @@ type CreatePopupPanelOpts struct {
HasLoader bool
Editable bool
Title string
Multiline bool
Prompt string
HandleConfirm func() error
HandleConfirmPrompt func(string) error
@@ -166,6 +167,7 @@ type CreatePopupPanelOpts struct {
type ConfirmOpts struct {
Title string
Multiline bool
Prompt string
HandleConfirm func() error
HandleClose func() error
@@ -176,6 +178,7 @@ type ConfirmOpts struct {
type PromptOpts struct {
Title string
Multiline bool
InitialContent string
FindSuggestionsFunc func(string) []*Suggestion
HandleConfirm func(string) error
+2 -2
View File
@@ -19,8 +19,8 @@ func (gui *Gui) resetViewOrigin(v *gocui.View) {
// that the scrollbar has the correct size, along with the number of lines after
// which the view is filled and we can do a first refresh.
func (gui *Gui) linesToReadFromCmdTask(v *gocui.View) tasks.LinesToRead {
height := v.InnerHeight()
oy := v.OriginY()
_, height := v.Size()
_, oy := v.Origin()
linesForFirstRefresh := height + oy + 10
+6 -30
View File
@@ -105,17 +105,12 @@ type TranslationSet struct {
NewBranchNameBranchOff string
CantDeleteCheckOutBranch string
DeleteBranchTitle string
DeleteBranchesTitle string
DeleteLocalBranch string
DeleteLocalBranches string
DeleteRemoteBranchOption string
DeleteRemoteBranchPrompt string
DeleteRemoteBranchesPrompt string
DeleteLocalAndRemoteBranchPrompt string
DeleteLocalAndRemoteBranchesPrompt string
ForceDeleteBranchTitle string
ForceDeleteBranchMessage string
ForceDeleteBranchesMessage string
RebaseBranch string
RebaseBranchTooltip string
CantRebaseOntoSelf string
@@ -140,7 +135,6 @@ type TranslationSet struct {
Quit string
SquashTooltip string
CannotSquashOrFixupFirstCommit string
CannotSquashOrFixupMergeCommit string
Fixup string
FixupTooltip string
SureFixupThisCommit string
@@ -161,7 +155,6 @@ type TranslationSet struct {
MoveDownCommit string
MoveUpCommit string
CannotMoveAnyFurther string
CannotMoveMergeCommit string
EditCommit string
EditCommitTooltip string
AmendCommitTooltip string
@@ -256,6 +249,8 @@ type TranslationSet struct {
NoBranchOnRemote string
Fetch string
FetchTooltip string
NoAutomaticGitFetchTitle string
NoAutomaticGitFetchBody string
FileEnter string
FileEnterTooltip string
FileStagingRequirements string
@@ -322,7 +317,6 @@ type TranslationSet struct {
YouDied string
RewordNotSupported string
ChangingThisActionIsNotAllowed string
DroppingMergeRequiresSingleSelection string
CherryPickCopy string
CherryPickCopyTooltip string
CherryPickCopyRangeTooltip string
@@ -350,7 +344,6 @@ type TranslationSet struct {
DropCommitTitle string
DropCommitPrompt string
DropUpdateRefPrompt string
DropMergeCommitPrompt string
PullingStatus string
PushingStatus string
FetchingStatus string
@@ -481,10 +474,8 @@ type TranslationSet struct {
RemoveRemoteTooltip string
RemoveRemotePrompt string
DeleteRemoteBranch string
DeleteRemoteBranches string
DeleteRemoteBranchTooltip string
DeleteLocalAndRemoteBranch string
DeleteLocalAndRemoteBranches string
SetAsUpstream string
SetAsUpstreamTooltip string
SetUpstream string
@@ -553,7 +544,6 @@ type TranslationSet struct {
ViewBranchUpstreamOptions string
ViewBranchUpstreamOptionsTooltip string
UpstreamNotSetError string
UpstreamsNotSetError string
NewGitFlowBranchPrompt string
RenameBranchWarning string
OpenKeybindingsMenu string
@@ -565,6 +555,7 @@ type TranslationSet struct {
MustStashWarning string
MustStashTitle string
ConfirmationTitle string
PressToSubmit string
PrevPage string
NextPage string
GotoTop string
@@ -696,8 +687,6 @@ type TranslationSet struct {
CreatePullRequestOptions string
DefaultBranch string
SelectBranch string
SelectTargetRemote string
NoValidRemoteName string
CreatePullRequest string
SelectConfigFile string
NoConfigFileFoundErr string
@@ -762,7 +751,6 @@ type TranslationSet struct {
SwitchToWorktreeTooltip string
AlreadyCheckedOutByWorktree string
BranchCheckedOutByWorktree string
SomeBranchesCheckedOutByWorktreeError string
DetachWorktreeTooltip string
Switching string
RemoveWorktree string
@@ -1100,17 +1088,12 @@ func EnglishTranslationSet() *TranslationSet {
NewBranchNameBranchOff: "New branch name (branch is off of '{{.branchName}}')",
CantDeleteCheckOutBranch: "You cannot delete the checked out branch!",
DeleteBranchTitle: "Delete branch '{{.selectedBranchName}}'?",
DeleteBranchesTitle: "Delete selected branches?",
DeleteLocalBranch: "Delete local branch",
DeleteLocalBranches: "Delete local branches",
DeleteRemoteBranchOption: "Delete remote branch",
DeleteRemoteBranchPrompt: "Are you sure you want to delete the remote branch '{{.selectedBranchName}}' from '{{.upstream}}'?",
DeleteRemoteBranchesPrompt: "Are you sure you want to delete the remote branches of the selected branches from their respective remotes?",
DeleteLocalAndRemoteBranchPrompt: "Are you sure you want to delete both '{{.localBranchName}}' from your machine, and '{{.remoteBranchName}}' from '{{.remoteName}}'?",
DeleteLocalAndRemoteBranchesPrompt: "Are you sure you want to delete both the selected branches from your machine, and their remote branches from their respective remotes?",
ForceDeleteBranchTitle: "Force delete branch",
ForceDeleteBranchMessage: "'{{.selectedBranchName}}' is not fully merged. Are you sure you want to delete it?",
ForceDeleteBranchesMessage: "Some of the selected branches are not fully merged. Are you sure you want to delete them?",
RebaseBranch: "Rebase",
RebaseBranchTooltip: "Rebase the checked-out branch onto the selected branch.",
CantRebaseOntoSelf: "You cannot rebase a branch onto itself",
@@ -1138,7 +1121,6 @@ func EnglishTranslationSet() *TranslationSet {
UpdateRefHere: "Update branch '{{.ref}}' here",
ExecCommandHere: "Execute the following command here:",
CannotSquashOrFixupFirstCommit: "There's no commit below to squash into",
CannotSquashOrFixupMergeCommit: "Cannot squash or fixup a merge commit",
Fixup: "Fixup",
SureFixupThisCommit: "Are you sure you want to 'fixup' the selected commit(s) into the commit below?",
SureSquashThisCommit: "Are you sure you want to squash the selected commit(s) into the commit below?",
@@ -1158,7 +1140,6 @@ func EnglishTranslationSet() *TranslationSet {
MoveDownCommit: "Move commit down one",
MoveUpCommit: "Move commit up one",
CannotMoveAnyFurther: "Cannot move any further",
CannotMoveMergeCommit: "Cannot move a merge commit",
EditCommit: "Edit (start interactive rebase)",
EditCommitTooltip: "Edit the selected commit. Use this to start an interactive rebase from the selected commit. When already mid-rebase, this will mark the selected commit for editing, which means that upon continuing the rebase, the rebase will pause at the selected commit to allow you to make changes.",
AmendCommitTooltip: "Amend commit with staged changes. If the selected commit is the HEAD commit, this will perform `git commit --amend`. Otherwise the commit will be amended via a rebase.",
@@ -1253,6 +1234,8 @@ func EnglishTranslationSet() *TranslationSet {
NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`,
Fetch: `Fetch`,
FetchTooltip: "Fetch changes from remote.",
NoAutomaticGitFetchTitle: `No automatic git fetch`,
NoAutomaticGitFetchBody: `Lazygit can't use "git fetch" in a private repo; use 'f' in the files panel to run "git fetch" manually`,
FileEnter: `Stage lines / Collapse directory`,
FileEnterTooltip: "If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it.",
FileStagingRequirements: `Can only stage individual lines for tracked files`,
@@ -1326,7 +1309,6 @@ func EnglishTranslationSet() *TranslationSet {
YouDied: "YOU DIED!",
RewordNotSupported: "Rewording commits while interactively rebasing is not currently supported",
ChangingThisActionIsNotAllowed: "Changing this kind of rebase todo entry is not allowed",
DroppingMergeRequiresSingleSelection: "Dropping a merge commit requires a single selected item",
CherryPickCopy: "Copy (cherry-pick)",
CherryPickCopyTooltip: "Mark commit as copied. Then, within the local commits view, you can press `{{.paste}}` to paste (cherry-pick) the copied commit(s) into your checked out branch. At any time you can press `{{.escape}}` to cancel the selection.",
CherryPickCopyRangeTooltip: "Mark commits as copied from the last copied commit to the selected commit.",
@@ -1353,7 +1335,6 @@ func EnglishTranslationSet() *TranslationSet {
AmendCommitPrompt: "Are you sure you want to amend this commit with your staged files?",
DropCommitTitle: "Drop commit",
DropCommitPrompt: "Are you sure you want to drop the selected commit(s)?",
DropMergeCommitPrompt: "Are you sure you want to drop the selected merge commit? Note that it will also drop all the commits that were merged in by it.",
DropUpdateRefPrompt: "Are you sure you want to delete the selected update-ref todo(s)? This is irreversible except by aborting the rebase.",
PullingStatus: "Pulling",
PushingStatus: "Pushing",
@@ -1486,10 +1467,8 @@ func EnglishTranslationSet() *TranslationSet {
RemoveRemoteTooltip: `Remove the selected remote. Any local branches tracking a remote branch from the remote will be unaffected.`,
RemoveRemotePrompt: "Are you sure you want to remove remote?",
DeleteRemoteBranch: "Delete remote branch",
DeleteRemoteBranches: "Delete remote branches",
DeleteRemoteBranchTooltip: "Delete the remote branch from the remote.",
DeleteLocalAndRemoteBranch: "Delete local and remote branch",
DeleteLocalAndRemoteBranches: "Delete local and remote branches",
SetAsUpstream: "Set as upstream",
SetAsUpstreamTooltip: "Set the selected remote branch as the upstream of the checked-out branch.",
SetUpstream: "Set upstream of selected branch",
@@ -1554,7 +1533,6 @@ func EnglishTranslationSet() *TranslationSet {
ViewBranchUpstreamOptions: "View upstream options",
ViewBranchUpstreamOptionsTooltip: "View options relating to the branch's upstream e.g. setting/unsetting the upstream and resetting to the upstream.",
UpstreamNotSetError: "The selected branch has no upstream (or the upstream is not stored locally)",
UpstreamsNotSetError: "Some of the selected branches have no upstream (or the upstream is not stored locally)",
Upstream: "Upstream",
UpstreamTooltip: "View upstream options for selected branch e.g. setting/unsetting the upstream and resetting to the upstream.",
NewBranchNamePrompt: "Enter new branch name for branch",
@@ -1568,6 +1546,7 @@ func EnglishTranslationSet() *TranslationSet {
MustStashWarning: "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
MustStashTitle: "Must stash",
ConfirmationTitle: "Confirmation panel",
PressToSubmit: "Press {{.Key}} to submit",
PrevPage: "Previous page",
NextPage: "Next page",
GotoTop: "Scroll to top",
@@ -1699,8 +1678,6 @@ func EnglishTranslationSet() *TranslationSet {
CreatePullRequestOptions: "View create pull request options",
DefaultBranch: "Default branch",
SelectBranch: "Select branch",
SelectTargetRemote: "Select target remote",
NoValidRemoteName: "A remote named '%s' does not exist",
SelectConfigFile: "Select config file",
NoConfigFileFoundErr: "No config file found",
LoadingFileSuggestions: "Loading file suggestions",
@@ -1766,7 +1743,6 @@ func EnglishTranslationSet() *TranslationSet {
SwitchToWorktreeTooltip: "Switch to the selected worktree.",
AlreadyCheckedOutByWorktree: "This branch is checked out by worktree {{.worktreeName}}. Do you want to switch to that worktree?",
BranchCheckedOutByWorktree: "Branch {{.branchName}} is checked out by worktree {{.worktreeName}}",
SomeBranchesCheckedOutByWorktreeError: "Some of the selected branches are checked out by other worktrees. Select them one by one to delete them.",
DetachWorktreeTooltip: "This will run `git checkout --detach` on the worktree so that it stops hogging the branch, but the worktree's working tree will be left alone.",
Switching: "Switching",
RemoveWorktree: "Remove worktree",
+1 -1
View File
@@ -203,7 +203,7 @@ func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) {
view := self.getView()
self.t.assertWithRetries(func() (bool, string) {
count := view.InnerHeight()
count := view.InnerHeight() + 1
return count == len(matchers), fmt.Sprintf("unexpected number of visible lines in view '%s'. Expected exactly %d, got %d", view.Name(), len(matchers), count)
})
}
+18 -6
View File
@@ -150,12 +150,24 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
Confirm()
}).
Tap(func() {
checkRemoteBranches(t, keys, "origin", []string{
"branch-five",
"branch-four",
"branch-six",
"branch-two",
})
t.Views().Remotes().
Focus().
Lines(Contains("origin")).
PressEnter()
t.Views().
RemoteBranches().
Lines(
Equals("branch-five"),
Equals("branch-four"),
Equals("branch-six"),
Equals("branch-two"),
).
Press(keys.Universal.Return)
t.Views().
Branches().
Focus()
}).
Lines(
Contains("current-head"),
@@ -1,186 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DeleteMultiple = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Try some combinations of local and remote branch deletions with a range selection of branches",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetAppState().LocalBranchSortOrder = "alphabetic"
},
SetupRepo: func(shell *Shell) {
shell.
CloneIntoRemote("origin").
CloneIntoRemote("other-remote").
EmptyCommit("blah").
NewBranch("branch-01").
EmptyCommit("on branch-01 01").
PushBranchAndSetUpstream("origin", "branch-01").
EmptyCommit("on branch-01 02").
NewBranch("branch-02").
EmptyCommit("on branch-02 01").
PushBranchAndSetUpstream("origin", "branch-02").
NewBranchFrom("branch-03", "master").
EmptyCommit("on branch-03 01").
NewBranch("current-head").
EmptyCommit("on current-head").
NewBranchFrom("branch-04", "master").
EmptyCommit("on branch-04 01").
PushBranchAndSetUpstream("other-remote", "branch-04").
EmptyCommit("on branch-04 02").
NewBranchFrom("branch-05", "master").
EmptyCommit("on branch-05 01").
PushBranchAndSetUpstream("origin", "branch-05").
NewBranchFrom("branch-06", "master").
EmptyCommit("on branch-06 01").
PushBranch("origin", "branch-06").
PushBranchAndSetUpstream("other-remote", "branch-06").
EmptyCommit("on branch-06 02").
Checkout("current-head")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("current-head").IsSelected(),
Contains("branch-01 ↑1"),
Contains("branch-02 ✓"),
Contains("branch-03"),
Contains("branch-04 ↑1"),
Contains("branch-05 ✓"),
Contains("branch-06 ↑1"),
Contains("master"),
).
Press(keys.Universal.RangeSelectDown).
// Deleting a range that includes the current branch is not possible
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Tooltip(Contains("You cannot delete the checked out branch!")).
Title(Equals("Delete selected branches?")).
Select(Contains("Delete local branches")).
Confirm().
Tap(func() {
t.ExpectToast(Contains("You cannot delete the checked out branch!"))
}).
Cancel()
}).
// Delete branch-03 and branch-04. 04 is not fully merged, so we get
// a confirmation popup.
NavigateToLine(Contains("branch-03")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected branches?")).
Select(Contains("Delete local branches")).
Confirm()
t.ExpectPopup().
Confirmation().
Title(Equals("Force delete branch")).
Content(Equals("Some of the selected branches are not fully merged. Are you sure you want to delete them?")).
Confirm()
}).
Lines(
Contains("current-head"),
Contains("branch-01 ↑1"),
Contains("branch-02 ✓"),
Contains("branch-05 ✓").IsSelected(),
Contains("branch-06 ↑1"),
Contains("master"),
).
// Delete remote branches of branch-05 and branch-06. They are on different remotes.
NavigateToLine(Contains("branch-05")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected branches?")).
Select(Contains("Delete remote branches")).
Confirm()
}).
Tap(func() {
t.ExpectPopup().
Confirmation().
Title(Equals("Delete selected branches?")).
Content(Equals("Are you sure you want to delete the remote branches of the selected branches from their respective remotes?")).
Confirm()
}).
Tap(func() {
checkRemoteBranches(t, keys, "origin", []string{
"branch-01",
"branch-02",
"branch-06",
})
checkRemoteBranches(t, keys, "other-remote", []string{
"branch-04",
})
}).
Lines(
Contains("current-head"),
Contains("branch-01 ↑1"),
Contains("branch-02 ✓"),
Contains("branch-05 (upstream gone)").IsSelected(),
Contains("branch-06 (upstream gone)").IsSelected(),
Contains("master"),
).
// Try to delete both local and remote branches of branch-02 and
// branch-05; not possible because branch-05's upstream is gone
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected branches?")).
Select(Contains("Delete local and remote branches")).
Confirm().
Tap(func() {
t.ExpectToast(Contains("Some of the selected branches have no upstream (or the upstream is not stored locally)"))
}).
Cancel()
}).
// Delete both local and remote branches of branch-01 and branch-02. We get
// the force-delete warning because branch-01 it is not fully merged.
NavigateToLine(Contains("branch-01")).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().
Menu().
Title(Equals("Delete selected branches?")).
Select(Contains("Delete local and remote branches")).
Confirm()
t.ExpectPopup().
Confirmation().
Title(Equals("Delete local and remote branch")).
Content(Contains("Are you sure you want to delete both the selected branches from your machine, and their remote branches from their respective remotes?").
Contains("Some of the selected branches are not fully merged. Are you sure you want to delete them?")).
Confirm()
}).
Lines(
Contains("current-head"),
Contains("branch-05 (upstream gone)").IsSelected(),
Contains("branch-06 (upstream gone)"),
Contains("master"),
).
Tap(func() {
checkRemoteBranches(t, keys, "origin", []string{
"branch-06",
})
checkRemoteBranches(t, keys, "other-remote", []string{
"branch-04",
})
})
},
})
@@ -1,54 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var OpenPullRequestInvalidTargetRemoteName = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open up a pull request, specifying a non-existing target remote",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// Create an initial commit ('git branch set-upstream-to' bails out otherwise)
shell.CreateFileAndAdd("file", "content1")
shell.Commit("one")
// Create a new branch
shell.NewBranch("branch-1")
// Create a couple of remotes
shell.CloneIntoRemote("upstream")
shell.CloneIntoRemote("origin")
// To allow a pull request to be created from a branch, it must have an upstream set.
shell.SetBranchUpstream("branch-1", "origin/branch-1")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Open a PR for the current branch (i.e. 'branch-1')
t.Views().
Branches().
Focus().
Press(keys.Branches.ViewPullRequestOptions)
t.ExpectPopup().
Menu().
Title(Equals("View create pull request options")).
Select(Contains("Select branch")).
Confirm()
// Verify that we're prompted to enter the remote and enter the name of a non-existing one.
t.ExpectPopup().
Prompt().
Title(Equals("Select target remote")).
Type("non-existing-remote").
Confirm()
// Verify that this leads to an error being shown (instead of progressing to branch selection).
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("A remote named 'non-existing-remote' does not exist")).
Confirm()
},
})
@@ -1,74 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var OpenPullRequestSelectRemoteAndTargetBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open up a pull request, specifying a remote and target branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().OS.OpenLink = "echo {{link}} > /tmp/openlink"
},
SetupRepo: func(shell *Shell) {
// Create an initial commit ('git branch set-upstream-to' bails out otherwise)
shell.CreateFileAndAdd("file", "content1")
shell.Commit("one")
// Create a new branch and a remote that has that branch
shell.NewBranch("branch-1")
shell.CloneIntoRemote("upstream")
// Create another branch and a second remote. The first remote doesn't have this branch.
shell.NewBranch("branch-2")
shell.CloneIntoRemote("origin")
// To allow a pull request to be created from a branch, it must have an upstream set.
shell.SetBranchUpstream("branch-2", "origin/branch-2")
shell.RunCommand([]string{"git", "remote", "set-url", "origin", "https://github.com/my-personal-fork/lazygit"})
shell.RunCommand([]string{"git", "remote", "set-url", "upstream", "https://github.com/jesseduffield/lazygit"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Open a PR for the current branch (i.e. 'branch-2')
t.Views().
Branches().
Focus().
Press(keys.Branches.ViewPullRequestOptions)
t.ExpectPopup().
Menu().
Title(Equals("View create pull request options")).
Select(Contains("Select branch")).
Confirm()
// Verify that we're prompted to enter the remote
t.ExpectPopup().
Prompt().
Title(Equals("Select target remote")).
SuggestionLines(
Equals("origin"),
Equals("upstream")).
ConfirmSuggestion(Equals("upstream"))
// Verify that we're prompted to enter the target branch and that only those branches
// present in the selected remote are listed as suggestions (i.e. 'branch-2' is not there).
t.ExpectPopup().
Prompt().
Title(Equals("branch-2 → upstream/")).
SuggestionLines(
Equals("branch-1"),
Equals("master")).
ConfirmSuggestion(Equals("master"))
// Verify that the expected URL is used (by checking the openlink file)
//
// Please note that when targeting a different remote - like it's done here in this test -
// the link is not yet correct. Thus, this test is expected to fail once this is fixed.
t.FileSystem().FileContent(
"/tmp/openlink",
Equals("https://github.com/my-personal-fork/lazygit/compare/master...branch-2?expand=1\n"))
},
})
-25
View File
@@ -1,25 +0,0 @@
package branch
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/samber/lo"
)
func checkRemoteBranches(t *TestDriver, keys config.KeybindingConfig, remoteName string, expectedBranches []string) {
t.Views().Remotes().
Focus().
NavigateToLine(Contains(remoteName)).
PressEnter()
t.Views().
RemoteBranches().
Lines(
lo.Map(expectedBranches, func(branch string, _ int) *TextMatcher { return Equals(branch) })...,
).
Press(keys.Universal.Return)
t.Views().
Branches().
Focus()
}
@@ -10,12 +10,7 @@ var CommitWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.GetUserConfig().Git.CommitPrefixes = map[string]config.CommitPrefixConfig{
"repo": {
Pattern: `^\w+/(\w+-\w+).*`,
Replace: "[$1]: ",
},
}
cfg.GetUserConfig().Git.CommitPrefixes = map[string]config.CommitPrefixConfig{"repo": {Pattern: "^\\w+\\/(\\w+-\\w+).*", Replace: "[$1]: "}}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("feature/TEST-001")
@@ -30,15 +25,6 @@ var CommitWithPrefix = NewIntegrationTest(NewIntegrationTestArgs{
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[TEST-001]: ")).
Cancel()
t.Views().Files().
IsFocused().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
InitialText(Equals("[TEST-001]: ")).
@@ -1,46 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
var DropMergeCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Drops a merge commit outside of an interactive rebase",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.22.0"), // first version that supports the --rebase-merges option
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shared.CreateMergeCommit(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI ⏣─╮ Merge branch 'second-change-branch' into first-change-branch").IsSelected(),
Contains("CI │ ◯ * second-change-branch unrelated change"),
Contains("CI │ ◯ second change"),
Contains("CI ◯ │ first change"),
Contains("CI ◯─╯ * original"),
Contains("CI ◯ three"),
Contains("CI ◯ two"),
Contains("CI ◯ one"),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Drop commit")).
Content(Equals("Are you sure you want to drop the selected merge commit? Note that it will also drop all the commits that were merged in by it.")).
Confirm()
}).
Lines(
Contains("CI ◯ first change").IsSelected(),
Contains("CI ◯ * original"),
Contains("CI ◯ three"),
Contains("CI ◯ two"),
Contains("CI ◯ one"),
)
},
})
@@ -38,6 +38,7 @@ var DropTodoCommitWithUpdateRef = NewIntegrationTest(NewIntegrationTestArgs{
).
NavigateToLine(Contains("commit 02")).
Press(keys.Universal.Edit).
Focus().
Lines(
Contains("pick").Contains("CI commit 07"),
Contains("pick").Contains("CI commit 06"),
@@ -1,55 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var EditAndAutoAmend = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Edit a commit, make a change and stage it, then continue the rebase to auto-amend the commit",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(3)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit 03"),
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToLine(Contains("commit 02")).
Press(keys.Universal.Edit).
Lines(
Contains("commit 03"),
MatchesRegexp("YOU ARE HERE.*commit 02").IsSelected(),
Contains("commit 01"),
)
t.Shell().CreateFile("fixup-file", "fixup content")
t.Views().Files().
Focus().
Press(keys.Files.RefreshFiles).
Lines(
Contains("??").Contains("fixup-file").IsSelected(),
).
PressPrimaryAction()
t.Common().ContinueRebase()
t.Views().Commits().
Focus().
Lines(
Contains("commit 03"),
Contains("commit 02").IsSelected(),
Contains("commit 01"),
)
t.Views().Main().
Content(Contains("fixup content"))
},
})
@@ -1,74 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var EditLastCommitOfStackedBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Edit and amend the last commit of a branch in a stack of branches, and ensure that it doesn't break the stack",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.38.0"),
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.MainBranches = []string{"master"}
config.GetAppState().GitLogShowGraph = "never"
},
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(1).
NewBranch("branch1").
CreateNCommitsStartingAt(2, 2).
NewBranch("branch2").
CreateNCommitsStartingAt(2, 4)
shell.SetConfig("rebase.updateRefs", "true")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI commit 05").IsSelected(),
Contains("CI commit 04"),
Contains("CI * commit 03"),
Contains("CI commit 02"),
Contains("CI commit 01"),
).
NavigateToLine(Contains("commit 03")).
Press(keys.Universal.Edit).
Lines(
Contains("pick").Contains("CI commit 05"),
Contains("pick").Contains("CI commit 04"),
Contains("update-ref").Contains("branch1"),
Contains("<-- YOU ARE HERE --- * commit 03").IsSelected(),
Contains("CI commit 02"),
Contains("CI commit 01"),
)
t.Shell().CreateFile("fixup-file", "fixup content")
t.Views().Files().
Focus().
Press(keys.Files.RefreshFiles).
Lines(
Contains("??").Contains("fixup-file").IsSelected(),
).
PressPrimaryAction().
Press(keys.Files.AmendLastCommit)
t.ExpectPopup().Confirmation().
Title(Equals("Amend last commit")).
Content(Contains("Are you sure you want to amend last commit?")).
Confirm()
t.Common().ContinueRebase()
t.Views().Commits().
Focus().
Lines(
Contains("CI commit 05"),
Contains("CI commit 04"),
Contains("CI * commit 03"),
Contains("CI commit 02"),
Contains("CI commit 01"),
)
},
})
@@ -1,43 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)
var EditRangeSelectDownToMergeOutsideRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Select a range of commits (the last one being a merge commit) to edit outside of a rebase",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.22.0"), // first version that supports the --rebase-merges option
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shared.CreateMergeCommit(shell)
shell.CreateNCommits(2)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
TopLines(
Contains("CI ◯ commit 02").IsSelected(),
Contains("CI ◯ commit 01"),
Contains("Merge branch 'second-change-branch' into first-change-branch"),
).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.RangeSelectDown).
Press(keys.Universal.Edit).
Lines(
Contains("edit CI commit 02").IsSelected(),
Contains("edit CI commit 01").IsSelected(),
Contains(" CI ⏣─╮ <-- YOU ARE HERE --- Merge branch 'second-change-branch' into first-change-branch").IsSelected(),
Contains(" CI │ ◯ * second-change-branch unrelated change"),
Contains(" CI │ ◯ second change"),
Contains(" CI ◯ │ first change"),
Contains(" CI ◯─╯ * original"),
Contains(" CI ◯ three"),
Contains(" CI ◯ two"),
Contains(" CI ◯ one"),
)
},
})
@@ -1,47 +0,0 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var MoveAcrossBranchBoundaryOutsideRebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a commit across a branch boundary in a stack of branches",
ExtraCmdArgs: []string{},
Skip: false,
GitVersion: AtLeast("2.38.0"),
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.MainBranches = []string{"master"}
config.GetAppState().GitLogShowGraph = "never"
},
SetupRepo: func(shell *Shell) {
shell.
CreateNCommits(1).
NewBranch("branch1").
CreateNCommitsStartingAt(2, 2).
NewBranch("branch2").
CreateNCommitsStartingAt(2, 4)
shell.SetConfig("rebase.updateRefs", "true")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("CI commit 05").IsSelected(),
Contains("CI commit 04"),
Contains("CI * commit 03"),
Contains("CI commit 02"),
Contains("CI commit 01"),
).
NavigateToLine(Contains("commit 04")).
Press(keys.Commits.MoveDownCommit).
Lines(
Contains("CI commit 05"),
Contains("CI * commit 03"),
Contains("CI commit 04").IsSelected(),
Contains("CI commit 02"),
Contains("CI commit 01"),
)
},
})
-8
View File
@@ -42,16 +42,13 @@ var tests = []*components.IntegrationTest{
branch.CheckoutByName,
branch.CreateTag,
branch.Delete,
branch.DeleteMultiple,
branch.DeleteRemoteBranchWithCredentialPrompt,
branch.DeleteRemoteBranchWithDifferentName,
branch.DetachedHead,
branch.NewBranchAutostash,
branch.NewBranchFromRemoteTrackingDifferentName,
branch.NewBranchFromRemoteTrackingSameName,
branch.OpenPullRequestInvalidTargetRemoteName,
branch.OpenPullRequestNoUpstream,
branch.OpenPullRequestSelectRemoteAndTargetBranch,
branch.OpenWithCliArg,
branch.Rebase,
branch.RebaseAbortOnConflict,
@@ -208,14 +205,10 @@ var tests = []*components.IntegrationTest{
interactive_rebase.DeleteUpdateRefTodo,
interactive_rebase.DontShowBranchHeadsForTodoItems,
interactive_rebase.DropCommitInCopiedBranchWithUpdateRef,
interactive_rebase.DropMergeCommit,
interactive_rebase.DropTodoCommitWithUpdateRef,
interactive_rebase.DropWithCustomCommentChar,
interactive_rebase.EditAndAutoAmend,
interactive_rebase.EditFirstCommit,
interactive_rebase.EditLastCommitOfStackedBranch,
interactive_rebase.EditNonTodoCommitDuringRebase,
interactive_rebase.EditRangeSelectDownToMergeOutsideRebase,
interactive_rebase.EditRangeSelectOutsideRebase,
interactive_rebase.EditTheConflCommit,
interactive_rebase.FixupFirstCommit,
@@ -223,7 +216,6 @@ var tests = []*components.IntegrationTest{
interactive_rebase.InteractiveRebaseOfCopiedBranch,
interactive_rebase.MidRebaseRangeSelect,
interactive_rebase.Move,
interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase,
interactive_rebase.MoveInRebase,
interactive_rebase.MoveUpdateRefTodo,
interactive_rebase.MoveWithCustomCommentChar,
-1
View File
@@ -20,7 +20,6 @@ func GetSchemaDir() string {
func GenerateSchema() {
schema := customReflect(&config.UserConfig{})
obj, _ := json.MarshalIndent(schema, "", " ")
obj = append(obj, '\n')
if err := os.WriteFile(GetSchemaDir()+"/config.json", obj, 0o644); err != nil {
fmt.Println("Error writing to file:", err)
-80
View File
@@ -3,8 +3,6 @@ package utils
import (
"bytes"
"strings"
"github.com/mattn/go-runewidth"
)
// SplitLines takes a multiline string and splits it on newlines
@@ -102,81 +100,3 @@ func ScanLinesAndTruncateWhenLongerThanBuffer(maxBufferSize int) func(data []byt
return 0, nil, nil
}
}
// Wrap lines to a given width, and return:
// - the wrapped lines
// - the line indices of the wrapped lines, indexed by the original line indices
// - the line indices of the original lines, indexed by the wrapped line indices
// If wrap is false, the text is returned as is.
// This code needs to behave the same as `gocui.lineWrap` does.
func WrapViewLinesToWidth(wrap bool, text string, width int) ([]string, []int, []int) {
lines := strings.Split(text, "\n")
if !wrap {
indices := make([]int, len(lines))
for i := range lines {
indices[i] = i
}
return lines, indices, indices
}
wrappedLines := make([]string, 0, len(lines))
wrappedLineIndices := make([]int, 0, len(lines))
originalLineIndices := make([]int, 0, len(lines))
for originalLineIdx, line := range lines {
wrappedLineIndices = append(wrappedLineIndices, len(wrappedLines))
// convert tabs to spaces
for i := 0; i < len(line); i++ {
if line[i] == '\t' {
numSpaces := 4 - (i % 4)
line = line[:i] + " "[:numSpaces] + line[i+1:]
i += numSpaces - 1
}
}
appendWrappedLine := func(str string) {
wrappedLines = append(wrappedLines, str)
originalLineIndices = append(originalLineIndices, originalLineIdx)
}
n := 0
offset := 0
lastWhitespaceIndex := -1
for i, currChr := range line {
rw := runewidth.RuneWidth(currChr)
n += rw
if n > width {
if currChr == ' ' {
appendWrappedLine(line[offset:i])
offset = i + 1
n = 0
} else if currChr == '-' {
appendWrappedLine(line[offset:i])
offset = i
n = rw
} else if lastWhitespaceIndex != -1 {
if line[lastWhitespaceIndex] == '-' {
appendWrappedLine(line[offset : lastWhitespaceIndex+1])
} else {
appendWrappedLine(line[offset:lastWhitespaceIndex])
}
offset = lastWhitespaceIndex + 1
n = runewidth.StringWidth(line[offset : i+1])
} else {
appendWrappedLine(line[offset:i])
offset = i
n = rw
}
lastWhitespaceIndex = -1
} else if currChr == ' ' || currChr == '-' {
lastWhitespaceIndex = i
}
}
appendWrappedLine(line[offset:])
}
return wrappedLines, wrappedLineIndices, originalLineIndices
}
-236
View File
@@ -5,7 +5,6 @@ import (
"strings"
"testing"
"github.com/jesseduffield/gocui"
"github.com/stretchr/testify/assert"
)
@@ -165,238 +164,3 @@ func TestScanLinesAndTruncateWhenLongerThanBuffer(t *testing.T) {
assert.EqualValues(t, s.expectedLines, result)
}
}
func TestWrapViewLinesToWidth(t *testing.T) {
tests := []struct {
name string
wrap bool
text string
width int
expectedWrappedLines []string
expectedWrappedLinesIndices []int
expectedOriginalLinesIndices []int
}{
{
name: "Wrap off",
wrap: false,
text: "1st line\n2nd line\n3rd line",
width: 5,
expectedWrappedLines: []string{
"1st line",
"2nd line",
"3rd line",
},
expectedWrappedLinesIndices: []int{0, 1, 2},
expectedOriginalLinesIndices: []int{0, 1, 2},
},
{
name: "Wrap on space",
wrap: true,
text: "Hello World",
width: 5,
expectedWrappedLines: []string{
"Hello",
"World",
},
expectedWrappedLinesIndices: []int{0},
expectedOriginalLinesIndices: []int{0, 0},
},
{
name: "Wrap on hyphen",
wrap: true,
text: "Hello-World",
width: 6,
expectedWrappedLines: []string{
"Hello-",
"World",
},
},
{
name: "Wrap on hyphen 2",
wrap: true,
text: "Blah Hello-World",
width: 12,
expectedWrappedLines: []string{
"Blah Hello-",
"World",
},
},
{
name: "Wrap on hyphen 3",
wrap: true,
text: "Blah Hello-World",
width: 11,
expectedWrappedLines: []string{
"Blah Hello-",
"World",
},
},
{
name: "Wrap on hyphen 4",
wrap: true,
text: "Blah Hello-World",
width: 10,
expectedWrappedLines: []string{
"Blah Hello",
"-World",
},
},
{
name: "Wrap on space 2",
wrap: true,
text: "Blah Hello World",
width: 10,
expectedWrappedLines: []string{
"Blah Hello",
"World",
},
},
{
name: "Wrap on space with more words",
wrap: true,
text: "Longer word here",
width: 10,
expectedWrappedLines: []string{
"Longer",
"word here",
},
},
{
name: "Split word that's too long",
wrap: true,
text: "ThisWordIsWayTooLong",
width: 10,
expectedWrappedLines: []string{
"ThisWordIs",
"WayTooLong",
},
},
{
name: "Split word that's too long over multiple lines",
wrap: true,
text: "ThisWordIsWayTooLong",
width: 5,
expectedWrappedLines: []string{
"ThisW",
"ordIs",
"WayTo",
"oLong",
},
},
{
name: "Lots of hyphens",
wrap: true,
text: "one-two-three-four-five",
width: 8,
expectedWrappedLines: []string{
"one-two-",
"three-",
"four-",
"five",
},
},
{
name: "Several lines using all the available width",
wrap: true,
text: "aaa bb cc ddd-ee ff",
width: 5,
expectedWrappedLines: []string{
"aaa",
"bb cc",
"ddd-",
"ee ff",
},
},
{
name: "Several lines using all the available width, with multi-cell runes",
wrap: true,
text: "🐤🐤🐤 🐝🐝 🙉🙉 🦊🦊🦊-🐬🐬 🦢🦢",
width: 9,
expectedWrappedLines: []string{
"🐤🐤🐤",
"🐝🐝 🙉🙉",
"🦊🦊🦊-",
"🐬🐬 🦢🦢",
},
},
{
name: "Space in last column",
wrap: true,
text: "hello world",
width: 6,
expectedWrappedLines: []string{
"hello",
"world",
},
},
{
name: "Hyphen in last column",
wrap: true,
text: "hello-world",
width: 6,
expectedWrappedLines: []string{
"hello-",
"world",
},
},
{
name: "English text",
wrap: true,
text: "+The sea reach of the Thames stretched before us like the bedinnind of an interminable waterway. In the offind the sea and the sky were welded todether without a joint, and in the luminous space the tanned sails of the bardes drifting blah blah",
width: 81,
expectedWrappedLines: []string{
"+The sea reach of the Thames stretched before us like the bedinnind of an",
"interminable waterway. In the offind the sea and the sky were welded todether",
"without a joint, and in the luminous space the tanned sails of the bardes",
"drifting blah blah",
},
},
{
name: "Tabs",
wrap: true,
text: "\ta\tbb\tccc\tdddd\teeeee",
width: 50,
expectedWrappedLines: []string{
" a bb ccc dddd eeeee",
},
},
{
name: "Multiple lines",
wrap: true,
text: "First paragraph\nThe second paragraph is a bit longer.\nThird paragraph\n",
width: 10,
expectedWrappedLines: []string{
"First",
"paragraph",
"The second",
"paragraph",
"is a bit",
"longer.",
"Third",
"paragraph",
"",
},
expectedWrappedLinesIndices: []int{0, 2, 6, 8},
expectedOriginalLinesIndices: []int{0, 0, 1, 1, 1, 1, 2, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wrappedLines, wrappedLinesIndices, originalLinesIndices := WrapViewLinesToWidth(tt.wrap, tt.text, tt.width)
assert.Equal(t, tt.expectedWrappedLines, wrappedLines)
if tt.expectedWrappedLinesIndices != nil {
assert.Equal(t, tt.expectedWrappedLinesIndices, wrappedLinesIndices)
}
if tt.expectedOriginalLinesIndices != nil {
assert.Equal(t, tt.expectedOriginalLinesIndices, originalLinesIndices)
}
// As a sanity check, also test that gocui's line wrapping behaves the same way
view := gocui.NewView("", 0, 0, tt.width+1, 1000, gocui.OutputNormal)
assert.Equal(t, tt.width, view.InnerWidth())
view.Wrap = tt.wrap
view.SetContent(tt.text)
assert.Equal(t, wrappedLines, view.ViewBufferLines())
})
}
}
+32 -52
View File
@@ -6,18 +6,24 @@ import (
"fmt"
"os"
"slices"
"strings"
"github.com/samber/lo"
"github.com/stefanhaller/git-todo-parser/todo"
)
type Todo struct {
Hash string // for todos that have one, e.g. pick, drop, fixup, etc.
Ref string // for update-ref todos
Hash string // for todos that have one, e.g. pick, drop, fixup, etc.
Ref string // for update-ref todos
Action todo.TodoCommand
}
// In order to change a TODO in git-rebase-todo, we need to specify the old action,
// because sometimes the same hash appears multiple times in the file (e.g. in a pick
// and later in a merge)
type TodoChange struct {
Hash string
OldAction todo.TodoCommand
NewAction todo.TodoCommand
}
@@ -34,7 +40,7 @@ func EditRebaseTodo(filePath string, changes []TodoChange, commentChar byte) err
t := &todos[i]
// This is a nested loop, but it's ok because the number of todos should be small
for _, change := range changes {
if equalHash(t.Commit, change.Hash) {
if t.Command == change.OldAction && equalHash(t.Commit, change.Hash) {
matchCount++
t.Command = change.NewAction
}
@@ -50,18 +56,18 @@ func EditRebaseTodo(filePath string, changes []TodoChange, commentChar byte) err
}
func equalHash(a, b string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
commonLength := min(len(a), len(b))
return commonLength > 0 && a[:commonLength] == b[:commonLength]
return strings.HasPrefix(a, b) || strings.HasPrefix(b, a)
}
func findTodo(todos []todo.Todo, todoToFind Todo) (int, bool) {
_, idx, ok := lo.FindIndexOf(todos, func(t todo.Todo) bool {
// For update-ref todos we also must compare the Ref (they have an empty hash)
return equalHash(t.Commit, todoToFind.Hash) && t.Ref == todoToFind.Ref
// Comparing just the hash is not enough; we need to compare both the
// action and the hash, as the hash could appear multiple times (e.g. in a
// pick and later in a merge). For update-ref todos we also must compare
// the Ref.
return t.Command == todoToFind.Action &&
equalHash(t.Commit, todoToFind.Hash) &&
t.Ref == todoToFind.Ref
})
return idx, ok
}
@@ -141,41 +147,41 @@ func deleteTodos(todos []todo.Todo, todosToDelete []Todo) ([]todo.Todo, error) {
return todos, nil
}
func MoveTodosDown(fileName string, todosToMove []Todo, isInRebase bool, commentChar byte) error {
func MoveTodosDown(fileName string, todosToMove []Todo, commentChar byte) error {
todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil {
return err
}
rearrangedTodos, err := moveTodosDown(todos, todosToMove, isInRebase)
rearrangedTodos, err := moveTodosDown(todos, todosToMove)
if err != nil {
return err
}
return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar)
}
func MoveTodosUp(fileName string, todosToMove []Todo, isInRebase bool, commentChar byte) error {
func MoveTodosUp(fileName string, todosToMove []Todo, commentChar byte) error {
todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil {
return err
}
rearrangedTodos, err := moveTodosUp(todos, todosToMove, isInRebase)
rearrangedTodos, err := moveTodosUp(todos, todosToMove)
if err != nil {
return err
}
return WriteRebaseTodoFile(fileName, rearrangedTodos, commentChar)
}
func moveTodoDown(todos []todo.Todo, todoToMove Todo, isInRebase bool) ([]todo.Todo, error) {
rearrangedTodos, err := moveTodoUp(lo.Reverse(todos), todoToMove, isInRebase)
func moveTodoDown(todos []todo.Todo, todoToMove Todo) ([]todo.Todo, error) {
rearrangedTodos, err := moveTodoUp(lo.Reverse(todos), todoToMove)
return lo.Reverse(rearrangedTodos), err
}
func moveTodosDown(todos []todo.Todo, todosToMove []Todo, isInRebase bool) ([]todo.Todo, error) {
rearrangedTodos, err := moveTodosUp(lo.Reverse(todos), lo.Reverse(todosToMove), isInRebase)
func moveTodosDown(todos []todo.Todo, todosToMove []Todo) ([]todo.Todo, error) {
rearrangedTodos, err := moveTodosUp(lo.Reverse(todos), lo.Reverse(todosToMove))
return lo.Reverse(rearrangedTodos), err
}
func moveTodoUp(todos []todo.Todo, todoToMove Todo, isInRebase bool) ([]todo.Todo, error) {
func moveTodoUp(todos []todo.Todo, todoToMove Todo) ([]todo.Todo, error) {
sourceIdx, ok := findTodo(todos, todoToMove)
if !ok {
@@ -188,7 +194,7 @@ func moveTodoUp(todos []todo.Todo, todoToMove Todo, isInRebase bool) ([]todo.Tod
// 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:], func(t todo.Todo) bool { return isRenderedTodo(t, isInRebase) })
_, skip, ok := lo.FindIndexOf(todos[sourceIdx+1:], isRenderedTodo)
if !ok {
// We expect callers to guard against this
@@ -202,10 +208,10 @@ func moveTodoUp(todos []todo.Todo, todoToMove Todo, isInRebase bool) ([]todo.Tod
return rearrangedTodos, nil
}
func moveTodosUp(todos []todo.Todo, todosToMove []Todo, isInRebase bool) ([]todo.Todo, error) {
func moveTodosUp(todos []todo.Todo, todosToMove []Todo) ([]todo.Todo, error) {
for _, todoToMove := range todosToMove {
var newTodos []todo.Todo
newTodos, err := moveTodoUp(todos, todoToMove, isInRebase)
newTodos, err := moveTodoUp(todos, todoToMove)
if err != nil {
return nil, err
}
@@ -286,33 +292,7 @@ func RemoveUpdateRefsForCopiedBranch(fileName string, commentChar byte) error {
}
// We render a todo in the commits view if it's a commit or if it's an
// update-ref or exec. We don't render label, reset, or comment lines.
func isRenderedTodo(t todo.Todo, isInRebase bool) bool {
return t.Commit != "" || (isInRebase && (t.Command == todo.UpdateRef || t.Command == todo.Exec))
}
func DropMergeCommit(fileName string, hash string, commentChar byte) error {
todos, err := ReadRebaseTodoFile(fileName, commentChar)
if err != nil {
return err
}
newTodos, err := dropMergeCommit(todos, hash)
if err != nil {
return err
}
return WriteRebaseTodoFile(fileName, newTodos, commentChar)
}
func dropMergeCommit(todos []todo.Todo, hash string) ([]todo.Todo, error) {
isMerge := func(t todo.Todo) bool {
return t.Command == todo.Merge && t.Flag == "-C" && equalHash(t.Commit, hash)
}
if lo.CountBy(todos, isMerge) != 1 {
return nil, fmt.Errorf("Expected exactly one merge commit with hash %s", hash)
}
_, idx, _ := lo.FindIndexOf(todos, isMerge)
return slices.Delete(todos, idx, idx+1), nil
// update-ref. We don't render label, reset, or comment lines.
func isRenderedTodo(t todo.Todo) bool {
return t.Commit != "" || t.Command == todo.UpdateRef
}
+19 -141
View File
@@ -2,7 +2,6 @@ package utils
import (
"errors"
"fmt"
"testing"
"github.com/stefanhaller/git-todo-parser/todo"
@@ -14,7 +13,6 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
testName string
todos []todo.Todo
todoToMoveDown Todo
isInRebase bool
expectedErr string
expectedTodos []todo.Todo
}
@@ -27,7 +25,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveDown: Todo{Hash: "5678"},
todoToMoveDown: Todo{Hash: "5678", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "5678"},
@@ -42,7 +40,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveDown: Todo{Hash: "abcd"},
todoToMoveDown: Todo{Hash: "abcd", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -57,7 +55,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
},
todoToMoveDown: Todo{Ref: "refs/heads/some_branch"},
todoToMoveDown: Todo{Ref: "refs/heads/some_branch", Action: todo.UpdateRef},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -65,54 +63,6 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
},
},
{
testName: "move across update-ref todo in rebase",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveDown: Todo{Hash: "5678"},
isInRebase: true,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Pick, Commit: "5678"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
},
},
{
testName: "move across update-ref todo outside of rebase",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveDown: Todo{Hash: "5678"},
isInRebase: false,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "1234"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
},
},
{
testName: "move across exec todo",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Exec, ExecCommand: "make test"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveDown: Todo{Hash: "5678"},
isInRebase: true,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Exec, ExecCommand: "make test"},
},
},
{
testName: "skip an invisible todo",
todos: []todo.Todo{
@@ -122,7 +72,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "def0"},
},
todoToMoveDown: Todo{Hash: "5678"},
todoToMoveDown: Todo{Hash: "5678", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -141,7 +91,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveDown: Todo{Hash: "def0"},
todoToMoveDown: Todo{Hash: "def0", Action: todo.Pick},
expectedErr: "Todo def0 not found in git-rebase-todo",
expectedTodos: []todo.Todo{},
},
@@ -152,7 +102,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveDown: Todo{Hash: "1234"},
todoToMoveDown: Todo{Hash: "1234", Action: todo.Pick},
expectedErr: "Destination position for moving todo is out of range",
expectedTodos: []todo.Todo{},
},
@@ -164,7 +114,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveDown: Todo{Hash: "1234"},
todoToMoveDown: Todo{Hash: "1234", Action: todo.Pick},
expectedErr: "Destination position for moving todo is out of range",
expectedTodos: []todo.Todo{},
},
@@ -172,7 +122,7 @@ func TestRebaseCommands_moveTodoDown(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
rearrangedTodos, err := moveTodoDown(s.todos, s.todoToMoveDown, s.isInRebase)
rearrangedTodos, err := moveTodoDown(s.todos, s.todoToMoveDown)
if s.expectedErr == "" {
assert.NoError(t, err)
} else {
@@ -189,7 +139,6 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
testName string
todos []todo.Todo
todoToMoveUp Todo
isInRebase bool
expectedErr string
expectedTodos []todo.Todo
}
@@ -202,7 +151,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveUp: Todo{Hash: "5678"},
todoToMoveUp: Todo{Hash: "5678", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -217,7 +166,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveUp: Todo{Hash: "1234"},
todoToMoveUp: Todo{Hash: "1234", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "5678"},
@@ -232,7 +181,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveUp: Todo{Ref: "refs/heads/some_branch"},
todoToMoveUp: Todo{Ref: "refs/heads/some_branch", Action: todo.UpdateRef},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -240,54 +189,6 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
},
},
{
testName: "move across update-ref todo in rebase",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveUp: Todo{Hash: "1234"},
isInRebase: true,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Pick, Commit: "5678"},
},
},
{
testName: "move across update-ref todo outside of rebase",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveUp: Todo{Hash: "1234"},
isInRebase: false,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.UpdateRef, Ref: "refs/heads/some_branch"},
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "1234"},
},
},
{
testName: "move across exec todo",
todos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Exec, ExecCommand: "make test"},
{Command: todo.Pick, Commit: "5678"},
},
todoToMoveUp: Todo{Hash: "1234"},
isInRebase: true,
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Exec, ExecCommand: "make test"},
{Command: todo.Pick, Commit: "1234"},
{Command: todo.Pick, Commit: "5678"},
},
},
{
testName: "skip an invisible todo",
todos: []todo.Todo{
@@ -297,7 +198,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "def0"},
},
todoToMoveUp: Todo{Hash: "abcd"},
todoToMoveUp: Todo{Hash: "abcd", Action: todo.Pick},
expectedErr: "",
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -316,7 +217,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveUp: Todo{Hash: "def0"},
todoToMoveUp: Todo{Hash: "def0", Action: todo.Pick},
expectedErr: "Todo def0 not found in git-rebase-todo",
expectedTodos: []todo.Todo{},
},
@@ -327,7 +228,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
{Command: todo.Pick, Commit: "abcd"},
},
todoToMoveUp: Todo{Hash: "abcd"},
todoToMoveUp: Todo{Hash: "abcd", Action: todo.Pick},
expectedErr: "Destination position for moving todo is out of range",
expectedTodos: []todo.Todo{},
},
@@ -339,7 +240,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
{Command: todo.Label, Label: "myLabel"},
{Command: todo.Reset, Label: "otherlabel"},
},
todoToMoveUp: Todo{Hash: "5678"},
todoToMoveUp: Todo{Hash: "5678", Action: todo.Pick},
expectedErr: "Destination position for moving todo is out of range",
expectedTodos: []todo.Todo{},
},
@@ -347,7 +248,7 @@ func TestRebaseCommands_moveTodoUp(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
rearrangedTodos, err := moveTodoUp(s.todos, s.todoToMoveUp, s.isInRebase)
rearrangedTodos, err := moveTodoUp(s.todos, s.todoToMoveUp)
if s.expectedErr == "" {
assert.NoError(t, err)
} else {
@@ -515,8 +416,8 @@ func TestRebaseCommands_deleteTodos(t *testing.T) {
{Command: todo.Pick, Commit: "abcd"},
},
todosToDelete: []Todo{
{Ref: "refs/heads/some_branch"},
{Hash: "abcd"},
{Ref: "refs/heads/some_branch", Action: todo.UpdateRef},
{Hash: "abcd", Action: todo.Pick},
},
expectedTodos: []todo.Todo{
{Command: todo.Pick, Commit: "1234"},
@@ -531,7 +432,7 @@ func TestRebaseCommands_deleteTodos(t *testing.T) {
{Command: todo.Pick, Commit: "5678"},
},
todosToDelete: []Todo{
{Hash: "abcd"},
{Hash: "abcd", Action: todo.Pick},
},
expectedTodos: []todo.Todo{},
expectedErr: errors.New("Todo abcd not found in git-rebase-todo"),
@@ -552,26 +453,3 @@ func TestRebaseCommands_deleteTodos(t *testing.T) {
})
}
}
func Test_equalHash(t *testing.T) {
scenarios := []struct {
a string
b string
expected bool
}{
{"", "", true},
{"", "123", false},
{"123", "", false},
{"123", "123", true},
{"123", "123abc", true},
{"123abc", "123", true},
{"123", "a", false},
{"1", "abc", false},
}
for _, scenario := range scenarios {
t.Run(fmt.Sprintf("'%s' vs. '%s'", scenario.a, scenario.b), func(t *testing.T) {
assert.Equal(t, scenario.expected, equalHash(scenario.a, scenario.b))
})
}
}
+2 -12
View File
@@ -96,11 +96,6 @@
"description": "How the window is split when in half screen mode (i.e. after hitting '+' once).\nPossible values:\n- 'left': split the window horizontally (side panel on the left, main view on the right)\n- 'top': split the window vertically (side panel on top, main view below)",
"default": "left"
},
"wrapLinesInStagingView": {
"type": "boolean",
"description": "If true, wrap lines in the staging view to the width of the view. This\nmakes it much easier to work with diffs that have long lines, e.g.\nparagraphs of markdown text.",
"default": true
},
"language": {
"type": "string",
"enum": [
@@ -298,11 +293,6 @@
"description": "If true, display the files in the file views as a tree. If false, display the files as a flat list.\nThis can be toggled from within Lazygit with the '~' key, but that will not change the default.",
"default": true
},
"showNumstatInFilesView": {
"type": "boolean",
"description": "If true, show the number of lines changed per file in the Files view",
"default": false
},
"showRandomTip": {
"type": "boolean",
"description": "If true, show a random tip in the command log when Lazygit starts",
@@ -610,7 +600,7 @@
},
"allBranchesLogCmd": {
"type": "string",
"description": "Command used to display git log of all branches in the main window.\nDeprecated: Use `allBranchesLogCmds` instead.",
"description": "Command used to display git log of all branches in the main window.\nDeprecated: User `allBranchesLogCmds` instead.",
"default": "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
},
"allBranchesLogCmds": {
@@ -1735,4 +1725,4 @@
},
"additionalProperties": false,
"type": "object"
}
}
+2 -2
View File
@@ -335,7 +335,7 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
g.Mutexes.ViewsMutex.Lock()
v := NewView(name, x0, y0, x1, y1, g.outputMode)
v := newView(name, x0, y0, x1, y1, g.outputMode)
v.BgColor, v.FgColor = g.BgColor, g.FgColor
v.SelBgColor, v.SelFgColor = g.SelBgColor, g.SelFgColor
v.Overlaps = overlaps
@@ -912,7 +912,7 @@ func calcScrollbarRune(
}
func calcRealScrollbarStartEnd(v *View) (bool, int, int) {
height := v.InnerHeight()
height := v.InnerHeight() + 1
fullHeight := v.ViewLinesHeight() - v.scrollMargin()
if v.CanScrollPastBottom {
+64 -52
View File
@@ -322,9 +322,14 @@ func (v *View) FocusPoint(cx int, cy int) {
if cy < 0 || cy > lineCount {
return
}
height := v.InnerHeight()
_, height := v.Size()
v.oy = calculateNewOrigin(cy, v.oy, lineCount, height)
ly := height - 1
if ly < 0 {
ly = 0
}
v.oy = calculateNewOrigin(cy, v.oy, lineCount, ly)
v.cx = cx
v.cy = cy - v.oy
}
@@ -338,16 +343,16 @@ func (v *View) CancelRangeSelect() {
}
func calculateNewOrigin(selectedLine int, oldOrigin int, lineCount int, viewHeight int) int {
if viewHeight >= lineCount {
if viewHeight > lineCount {
return 0
} else if selectedLine < oldOrigin || selectedLine >= oldOrigin+viewHeight {
} else if selectedLine < oldOrigin || selectedLine > oldOrigin+viewHeight {
// If the selected line is outside the visible area, scroll the view so
// that the selected line is in the middle.
newOrigin := selectedLine - viewHeight/2
// However, take care not to overflow if the total line count is less
// than the view height.
maxOrigin := lineCount - viewHeight
maxOrigin := lineCount - viewHeight - 1
if newOrigin > maxOrigin {
newOrigin = maxOrigin
}
@@ -402,8 +407,8 @@ func (l lineType) String() string {
return str
}
// NewView returns a new View object.
func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
// newView returns a new View object.
func newView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
v := &View{
name: name,
x0: x0,
@@ -433,32 +438,22 @@ func (v *View) Dimensions() (int, int, int, int) {
return v.x0, v.y0, v.x1, v.y1
}
// Size returns the number of visible columns and rows in the View, including
// the frame if any
// Size returns the number of visible columns and rows in the View.
func (v *View) Size() (x, y int) {
return v.Width(), v.Height()
}
// InnerSize returns the number of usable columns and rows in the View, excluding
// the frame if any
func (v *View) InnerSize() (x, y int) {
return v.InnerWidth(), v.InnerHeight()
}
func (v *View) Width() int {
return v.x1 - v.x0 + 1
return v.x1 - v.x0 - 1
}
func (v *View) Height() int {
return v.y1 - v.y0 + 1
return v.y1 - v.y0 - 1
}
// The writeable area of the view is always two less then the view's size,
// because if it has a frame, we need to subtract that, but if it doesn't, the
// view is made 1 larger on all sides. I'd like to clean this up at some point,
// but for now we live with this weirdness.
// if a view has a frame, that leaves less space for its writeable area
func (v *View) InnerWidth() int {
innerWidth := v.Width() - 2
innerWidth := v.Width() - v.frameOffset()
if innerWidth < 0 {
return 0
}
@@ -467,7 +462,7 @@ func (v *View) InnerWidth() int {
}
func (v *View) InnerHeight() int {
innerHeight := v.Height() - 2
innerHeight := v.Height() - v.frameOffset()
if innerHeight < 0 {
return 0
}
@@ -475,6 +470,14 @@ func (v *View) InnerHeight() int {
return innerHeight
}
func (v *View) frameOffset() int {
if v.Frame {
return 1
} else {
return 0
}
}
// Name returns the name of the view.
func (v *View) Name() string {
return v.name
@@ -494,15 +497,31 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) {
bgColor = v.BgColor
ch = v.Mask
} else if v.Highlight {
rangeSelectStart := v.cy
rangeSelectEnd := v.cy
if v.rangeSelectStartY != -1 {
relativeRangeSelectStart := v.rangeSelectStartY - v.oy
rangeSelectStart = min(relativeRangeSelectStart, v.cy)
rangeSelectEnd = max(relativeRangeSelectStart, v.cy)
var ry, rcy int
_, ry, ok := v.realPosition(x, y)
if !ok {
return
}
_, rrcy, ok := v.realPosition(v.cx, v.cy)
// out of bounds is fine
if ok {
rcy = rrcy
}
if y >= rangeSelectStart && y <= rangeSelectEnd {
rangeSelectStart := rcy
rangeSelectEnd := rcy
if v.rangeSelectStartY != -1 {
_, realRangeSelectStart, ok := v.realPosition(0, v.rangeSelectStartY-v.oy)
if !ok {
return
}
rangeSelectStart = min(realRangeSelectStart, rcy)
rangeSelectEnd = max(realRangeSelectStart, rcy)
}
if ry >= rangeSelectStart && ry <= rangeSelectEnd {
// this ensures we use the bright variant of a colour upon highlight
fgColorComponent := fgColor & ^AttrAll
if fgColorComponent >= AttrIsValidColor && fgColorComponent < AttrIsValidColor+8 {
@@ -554,7 +573,7 @@ func max(a, b int) int {
// SetCursor sets the cursor position of the view at the given point,
// relative to the view. It checks if the position is valid.
func (v *View) SetCursor(x, y int) {
maxX, maxY := v.InnerSize()
maxX, maxY := v.Size()
if x < 0 || x >= maxX || y < 0 || y >= maxY {
return
}
@@ -563,7 +582,7 @@ func (v *View) SetCursor(x, y int) {
}
func (v *View) SetCursorX(x int) {
maxX := v.InnerWidth()
maxX, _ := v.Size()
if x < 0 || x >= maxX {
return
}
@@ -571,7 +590,7 @@ func (v *View) SetCursorX(x int) {
}
func (v *View) SetCursorY(y int) {
maxY := v.InnerHeight()
_, maxY := v.Size()
if y < 0 || y >= maxY {
return
}
@@ -898,7 +917,7 @@ func (v *View) parseInput(ch rune, x int, _ int) (bool, []cell) {
for _, cell := range v.lines[v.wy][0:v.wx] {
cx += runewidth.RuneWidth(cell.chr)
}
repeatCount = v.InnerWidth() - cx
repeatCount = v.InnerWidth() - cx + 1
ch = ' '
truncateLine = true
} else if isEscape {
@@ -1087,8 +1106,6 @@ func (v *View) updateSearchPositions() {
if v.searcher.modelSearchResults != nil {
for _, result := range v.searcher.modelSearchResults {
// This code only works when v.Wrap is false.
if result.Y >= len(v.lines) {
break
}
@@ -1117,9 +1134,8 @@ func (v *View) updateSearchPositions() {
}
}
} else {
v.refreshViewLinesIfNeeded()
for y, line := range v.viewLines {
v.searcher.searchPositions = append(v.searcher.searchPositions, searchPositionsForLine(line.line, y)...)
for y, line := range v.lines {
v.searcher.searchPositions = append(v.searcher.searchPositions, searchPositionsForLine(line, y)...)
}
}
}
@@ -1141,7 +1157,7 @@ func (v *View) draw() {
v.clearRunes()
maxX, maxY := v.InnerSize()
maxX, maxY := v.Size()
if v.Wrap {
if maxX == 0 {
@@ -1234,7 +1250,7 @@ func (v *View) draw() {
func (v *View) refreshViewLinesIfNeeded() {
if v.tainted {
maxX := v.InnerWidth()
maxX := v.Width()
lineIdx := 0
lines := v.lines
if v.HasLoader {
@@ -1325,7 +1341,7 @@ func (v *View) realPosition(vx, vy int) (x, y int, ok bool) {
// clearRunes erases all the cells in the view.
func (v *View) clearRunes() {
maxX, maxY := v.InnerSize()
maxX, maxY := v.Size()
for x := 0; x < maxX; x++ {
for y := 0; y < maxY; y++ {
tcellSetCell(v.x0+x+1, v.y0+y+1, ' ', v.FgColor, v.BgColor, v.outMode)
@@ -1360,8 +1376,6 @@ func (v *View) ViewBufferLines() []string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
v.refreshViewLinesIfNeeded()
lines := make([]string, len(v.viewLines))
for i, l := range v.viewLines {
str := lineType(l.line).String()
@@ -1501,20 +1515,18 @@ func lineWrap(line []cell, columns int) [][]cell {
lines = append(lines, line[offset:i])
offset = i
n = rw
} else if lastWhitespaceIndex != -1 {
} else if lastWhitespaceIndex != -1 && lastWhitespaceIndex+1 != i {
// if there is a space in the line and the line is not breaking at a space/hyphen
if line[lastWhitespaceIndex].chr == '-' {
// if break occurs at hyphen, we'll retain the hyphen
lines = append(lines, line[offset:lastWhitespaceIndex+1])
offset = lastWhitespaceIndex + 1
n = i - offset
} else {
// if break occurs at space, we'll omit the space
lines = append(lines, line[offset:lastWhitespaceIndex])
}
// Either way, continue *after* the break
offset = lastWhitespaceIndex + 1
n = 0
for _, c := range line[offset : i+1] {
n += runewidth.RuneWidth(c.chr)
offset = lastWhitespaceIndex + 1
n = i - offset + 1
}
} else {
// in this case we're breaking mid-word
@@ -1777,7 +1789,7 @@ func (v *View) adjustDownwardScrollAmount(scrollHeight int) int {
_, oy := v.Origin()
y := oy
if !v.CanScrollPastBottom {
sy := v.InnerHeight()
_, sy := v.Size()
y += sy
}
scrollableLines := v.ViewLinesHeight() - y
-17
View File
@@ -1,17 +0,0 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin && amd64 && gc
#include "textflag.h"
TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_sysctl(SB)
GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8
DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB)
TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0
JMP libc_sysctlbyname(SB)
GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8
DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB)
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin && amd64 && gc
package cpu
// darwinSupportsAVX512 checks Darwin kernel for AVX512 support via sysctl
// call (see issue 43089). It also restricts AVX512 support for Darwin to
// kernel version 21.3.0 (MacOS 12.2.0) or later (see issue 49233).
//
// Background:
// Darwin implements a special mechanism to economize on thread state when
// AVX512 specific registers are not in use. This scheme minimizes state when
// preempting threads that haven't yet used any AVX512 instructions, but adds
// special requirements to check for AVX512 hardware support at runtime (e.g.
// via sysctl call or commpage inspection). See issue 43089 and link below for
// full background:
// https://github.com/apple-oss-distributions/xnu/blob/xnu-11215.1.10/osfmk/i386/fpu.c#L214-L240
//
// Additionally, all versions of the Darwin kernel from 19.6.0 through 21.2.0
// (corresponding to MacOS 10.15.6 - 12.1) have a bug that can cause corruption
// of the AVX512 mask registers (K0-K7) upon signal return. For this reason
// AVX512 is considered unsafe to use on Darwin for kernel versions prior to
// 21.3.0, where a fix has been confirmed. See issue 49233 for full background.
func darwinSupportsAVX512() bool {
return darwinSysctlEnabled([]byte("hw.optional.avx512f\x00")) && darwinKernelVersionCheck(21, 3, 0)
}
// Ensure Darwin kernel version is at least major.minor.patch, avoiding dependencies
func darwinKernelVersionCheck(major, minor, patch int) bool {
var release [256]byte
err := darwinOSRelease(&release)
if err != nil {
return false
}
var mmp [3]int
c := 0
Loop:
for _, b := range release[:] {
switch {
case b >= '0' && b <= '9':
mmp[c] = 10*mmp[c] + int(b-'0')
case b == '.':
c++
if c > 2 {
return false
}
case b == 0:
break Loop
default:
return false
}
}
if c != 2 {
return false
}
return mmp[0] > major || mmp[0] == major && (mmp[1] > minor || mmp[1] == minor && mmp[2] >= patch)
}
+2 -2
View File
@@ -6,10 +6,10 @@
package cpu
// cpuid is implemented in cpu_gc_x86.s for gc compiler
// cpuid is implemented in cpu_x86.s for gc compiler
// and in cpu_gccgo.c for gccgo.
func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32)
// xgetbv with ecx = 0 is implemented in cpu_gc_x86.s for gc compiler
// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler
// and in cpu_gccgo.c for gccgo.
func xgetbv() (eax, edx uint32)
+6
View File
@@ -23,3 +23,9 @@ func xgetbv() (eax, edx uint32) {
gccgoXgetbv(&a, &d)
return a, d
}
// gccgo doesn't build on Darwin, per:
// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76
func darwinSupportsAVX512() bool {
return false
}
+1
View File
@@ -110,6 +110,7 @@ func doinit() {
ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM)
ARM64.HasDIT = isSet(hwCap, hwcap_DIT)
// HWCAP2 feature bits
ARM64.HasSVE2 = isSet(hwCap2, hwcap2_SVE2)
ARM64.HasI8MM = isSet(hwCap2, hwcap2_I8MM)
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build 386 || amd64p32 || (amd64 && (!darwin || !gc))
package cpu
func darwinSupportsAVX512() bool {
panic("only implemented for gc && amd64 && darwin")
}
+4 -2
View File
@@ -92,8 +92,10 @@ func archInit() {
osSupportsAVX = isSet(1, eax) && isSet(2, eax)
if runtime.GOOS == "darwin" {
// Darwin requires special AVX512 checks, see cpu_darwin_x86.go
osSupportsAVX512 = osSupportsAVX && darwinSupportsAVX512()
// Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers.
// Since users can't rely on mask register contents, let's not advertise AVX-512 support.
// See issue 49233.
osSupportsAVX512 = false
} else {
// Check if OPMASK and ZMM registers have OS support.
osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax)
@@ -18,7 +18,7 @@ TEXT ·cpuid(SB), NOSPLIT, $0-24
RET
// func xgetbv() (eax, edx uint32)
TEXT ·xgetbv(SB), NOSPLIT, $0-8
TEXT ·xgetbv(SB),NOSPLIT,$0-8
MOVL $0, CX
XGETBV
MOVL AX, eax+0(FP)
-98
View File
@@ -1,98 +0,0 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Minimal copy of x/sys/unix so the cpu package can make a
// system call on Darwin without depending on x/sys/unix.
//go:build darwin && amd64 && gc
package cpu
import (
"syscall"
"unsafe"
)
type _C_int int32
// adapted from unix.Uname() at x/sys/unix/syscall_darwin.go L419
func darwinOSRelease(release *[256]byte) error {
// from x/sys/unix/zerrors_openbsd_amd64.go
const (
CTL_KERN = 0x1
KERN_OSRELEASE = 0x2
)
mib := []_C_int{CTL_KERN, KERN_OSRELEASE}
n := unsafe.Sizeof(*release)
return sysctl(mib, &release[0], &n, nil, 0)
}
type Errno = syscall.Errno
var _zero uintptr // Single-word zero for use when we need a valid pointer to 0 bytes.
// from x/sys/unix/zsyscall_darwin_amd64.go L791-807
func sysctl(mib []_C_int, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error {
var _p0 unsafe.Pointer
if len(mib) > 0 {
_p0 = unsafe.Pointer(&mib[0])
} else {
_p0 = unsafe.Pointer(&_zero)
}
if _, _, err := syscall_syscall6(
libc_sysctl_trampoline_addr,
uintptr(_p0),
uintptr(len(mib)),
uintptr(unsafe.Pointer(old)),
uintptr(unsafe.Pointer(oldlen)),
uintptr(unsafe.Pointer(new)),
uintptr(newlen),
); err != 0 {
return err
}
return nil
}
var libc_sysctl_trampoline_addr uintptr
// adapted from internal/cpu/cpu_arm64_darwin.go
func darwinSysctlEnabled(name []byte) bool {
out := int32(0)
nout := unsafe.Sizeof(out)
if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil {
return false
}
return out > 0
}
//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib"
var libc_sysctlbyname_trampoline_addr uintptr
// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix
func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error {
if _, _, err := syscall_syscall6(
libc_sysctlbyname_trampoline_addr,
uintptr(unsafe.Pointer(name)),
uintptr(unsafe.Pointer(old)),
uintptr(unsafe.Pointer(oldlen)),
uintptr(unsafe.Pointer(new)),
uintptr(newlen),
0,
); err != 0 {
return err
}
return nil
}
//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib"
// Implemented in the runtime package (runtime/sys_darwin.go)
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)
//go:linkname syscall_syscall6 syscall.syscall6
+1 -1
View File
@@ -156,7 +156,7 @@ from the generated architecture-specific files listed below, and merge these
into a common file for each OS.
The merge is performed in the following steps:
1. Construct the set of common code that is identical in all architecture-specific files.
1. Construct the set of common code that is idential in all architecture-specific files.
2. Write this common code to the merged file.
3. Remove the common code from all architecture-specific files.
-96
View File
@@ -58,102 +58,6 @@ func IoctlGetEthtoolDrvinfo(fd int, ifname string) (*EthtoolDrvinfo, error) {
return &value, err
}
// IoctlGetEthtoolTsInfo fetches ethtool timestamping and PHC
// association for the network device specified by ifname.
func IoctlGetEthtoolTsInfo(fd int, ifname string) (*EthtoolTsInfo, error) {
ifr, err := NewIfreq(ifname)
if err != nil {
return nil, err
}
value := EthtoolTsInfo{Cmd: ETHTOOL_GET_TS_INFO}
ifrd := ifr.withData(unsafe.Pointer(&value))
err = ioctlIfreqData(fd, SIOCETHTOOL, &ifrd)
return &value, err
}
// IoctlGetHwTstamp retrieves the hardware timestamping configuration
// for the network device specified by ifname.
func IoctlGetHwTstamp(fd int, ifname string) (*HwTstampConfig, error) {
ifr, err := NewIfreq(ifname)
if err != nil {
return nil, err
}
value := HwTstampConfig{}
ifrd := ifr.withData(unsafe.Pointer(&value))
err = ioctlIfreqData(fd, SIOCGHWTSTAMP, &ifrd)
return &value, err
}
// IoctlSetHwTstamp updates the hardware timestamping configuration for
// the network device specified by ifname.
func IoctlSetHwTstamp(fd int, ifname string, cfg *HwTstampConfig) error {
ifr, err := NewIfreq(ifname)
if err != nil {
return err
}
ifrd := ifr.withData(unsafe.Pointer(cfg))
return ioctlIfreqData(fd, SIOCSHWTSTAMP, &ifrd)
}
// FdToClockID derives the clock ID from the file descriptor number
// - see clock_gettime(3), FD_TO_CLOCKID macros. The resulting ID is
// suitable for system calls like ClockGettime.
func FdToClockID(fd int) int32 { return int32((int(^fd) << 3) | 3) }
// IoctlPtpClockGetcaps returns the description of a given PTP device.
func IoctlPtpClockGetcaps(fd int) (*PtpClockCaps, error) {
var value PtpClockCaps
err := ioctlPtr(fd, PTP_CLOCK_GETCAPS2, unsafe.Pointer(&value))
return &value, err
}
// IoctlPtpSysOffsetPrecise returns a description of the clock
// offset compared to the system clock.
func IoctlPtpSysOffsetPrecise(fd int) (*PtpSysOffsetPrecise, error) {
var value PtpSysOffsetPrecise
err := ioctlPtr(fd, PTP_SYS_OFFSET_PRECISE2, unsafe.Pointer(&value))
return &value, err
}
// IoctlPtpSysOffsetExtended returns an extended description of the
// clock offset compared to the system clock. The samples parameter
// specifies the desired number of measurements.
func IoctlPtpSysOffsetExtended(fd int, samples uint) (*PtpSysOffsetExtended, error) {
value := PtpSysOffsetExtended{Samples: uint32(samples)}
err := ioctlPtr(fd, PTP_SYS_OFFSET_EXTENDED2, unsafe.Pointer(&value))
return &value, err
}
// IoctlPtpPinGetfunc returns the configuration of the specified
// I/O pin on given PTP device.
func IoctlPtpPinGetfunc(fd int, index uint) (*PtpPinDesc, error) {
value := PtpPinDesc{Index: uint32(index)}
err := ioctlPtr(fd, PTP_PIN_GETFUNC2, unsafe.Pointer(&value))
return &value, err
}
// IoctlPtpPinSetfunc updates configuration of the specified PTP
// I/O pin.
func IoctlPtpPinSetfunc(fd int, pd *PtpPinDesc) error {
return ioctlPtr(fd, PTP_PIN_SETFUNC2, unsafe.Pointer(pd))
}
// IoctlPtpPeroutRequest configures the periodic output mode of the
// PTP I/O pins.
func IoctlPtpPeroutRequest(fd int, r *PtpPeroutRequest) error {
return ioctlPtr(fd, PTP_PEROUT_REQUEST2, unsafe.Pointer(r))
}
// IoctlPtpExttsRequest configures the external timestamping mode
// of the PTP I/O pins.
func IoctlPtpExttsRequest(fd int, r *PtpExttsRequest) error {
return ioctlPtr(fd, PTP_EXTTS_REQUEST2, unsafe.Pointer(r))
}
// IoctlGetWatchdogInfo fetches information about a watchdog device from the
// Linux watchdog API. For more information, see:
// https://www.kernel.org/doc/html/latest/watchdog/watchdog-api.html.
+2 -14
View File
@@ -158,16 +158,6 @@ includes_Linux='
#endif
#define _GNU_SOURCE
// See the description in unix/linux/types.go
#if defined(__ARM_EABI__) || \
(defined(__mips__) && (_MIPS_SIM == _ABIO32)) || \
(defined(__powerpc__) && (!defined(__powerpc64__)))
# ifdef _TIME_BITS
# undef _TIME_BITS
# endif
# define _TIME_BITS 32
#endif
// <sys/ioctl.h> is broken on powerpc64, as it fails to include definitions of
// these structures. We just include them copied from <bits/termios.h>.
#if defined(__powerpc__)
@@ -266,7 +256,6 @@ struct ltchars {
#include <linux/nsfs.h>
#include <linux/perf_event.h>
#include <linux/pps.h>
#include <linux/ptp_clock.h>
#include <linux/ptrace.h>
#include <linux/random.h>
#include <linux/reboot.h>
@@ -538,7 +527,6 @@ ccflags="$@"
$2 ~ /^(AF|SOCK|SO|SOL|IPPROTO|IP|IPV6|TCP|MCAST|EVFILT|NOTE|SHUT|PROT|MAP|MREMAP|MFD|T?PACKET|MSG|SCM|MCL|DT|MADV|PR|LOCAL|TCPOPT|UDP)_/ ||
$2 ~ /^NFC_(GENL|PROTO|COMM|RF|SE|DIRECTION|LLCP|SOCKPROTO)_/ ||
$2 ~ /^NFC_.*_(MAX)?SIZE$/ ||
$2 ~ /^PTP_/ ||
$2 ~ /^RAW_PAYLOAD_/ ||
$2 ~ /^[US]F_/ ||
$2 ~ /^TP_STATUS_/ ||
@@ -668,7 +656,7 @@ errors=$(
signals=$(
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print $2 }' |
grep -E -v '(SIGSTKSIZE|SIGSTKSZ|SIGRT|SIGMAX64)' |
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT\|SIGMAX64' |
sort
)
@@ -678,7 +666,7 @@ echo '#include <errno.h>' | $CC -x c - -E -dM $ccflags |
sort >_error.grep
echo '#include <signal.h>' | $CC -x c - -E -dM $ccflags |
awk '$1=="#define" && $2 ~ /^SIG[A-Z0-9]+$/ { print "^\t" $2 "[ \t]*=" }' |
grep -E -v '(SIGSTKSIZE|SIGSTKSZ|SIGRT|SIGMAX64)' |
grep -v 'SIGSTKSIZE\|SIGSTKSZ\|SIGRT\|SIGMAX64' |
sort >_signal.grep
echo '// mkerrors.sh' "$@"

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