mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 02:35:39 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b05ba252c | ||
|
|
28474b08ee | ||
|
|
6f13b42279 | ||
|
|
8637587b82 | ||
|
|
f581dc4a56 | ||
|
|
693e9fc152 | ||
|
|
c595833883 | ||
|
|
7807b40322 | ||
|
|
b284970bac | ||
|
|
b46623ebef | ||
|
|
084c0a19bc | ||
|
|
3cee37388c | ||
|
|
a7969aef2c | ||
|
|
39c900c7e7 | ||
|
|
6e247c1583 | ||
|
|
87bf1dbc7f | ||
|
|
1f920ae6ba | ||
|
|
932e01b41a | ||
|
|
373f24c80f | ||
|
|
a548b289ef | ||
|
|
b168fc8cdd | ||
|
|
94845dcf98 | ||
|
|
1af6dff64e | ||
|
|
72de4f436e | ||
|
|
effda8291b | ||
|
|
7985e31020 | ||
|
|
866e0a618b | ||
|
|
a5ee61c117 | ||
|
|
08aad924d7 | ||
|
|
be02786dad | ||
|
|
949022db8c | ||
|
|
b1a090d4c0 | ||
|
|
39f3f150ed |
@@ -0,0 +1,26 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
categories:
|
||||
- title: Features ✨
|
||||
labels:
|
||||
- feature
|
||||
- title: Enhancements 🔥
|
||||
labels:
|
||||
- enhancement
|
||||
- title: Fixes 🔧
|
||||
labels:
|
||||
- bug
|
||||
- title: Maintenance ⚙️
|
||||
labels:
|
||||
- maintenance
|
||||
- title: Docs 📖
|
||||
labels:
|
||||
- docs
|
||||
- title: I18n 🌎
|
||||
labels:
|
||||
- i18n
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -19,6 +19,10 @@ jobs:
|
||||
go-version: 1.18.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: v1.17.2
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
homebrew:
|
||||
|
||||
@@ -204,3 +204,11 @@ jobs:
|
||||
- name: errors
|
||||
run: golangci-lint run
|
||||
if: ${{ failure() }}
|
||||
check-required-label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: mheap/github-action-required-labels@v5
|
||||
with:
|
||||
mode: exactly
|
||||
count: 1
|
||||
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n"
|
||||
|
||||
@@ -39,5 +39,3 @@ test/results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
|
||||
.worktrees
|
||||
+1
-1
@@ -12,7 +12,7 @@ builds:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- 386
|
||||
- '386'
|
||||
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
eUTlaNqeTB
|
||||
@@ -1 +0,0 @@
|
||||
qtuVMihtEl
|
||||
@@ -18,7 +18,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.20230710004407-9bbfd873713b
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144
|
||||
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
|
||||
|
||||
@@ -72,8 +72,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.20230710004407-9bbfd873713b h1:8FmmdaYHes1m3oNyNdS+VIgkgkFpNZAWuwTnvp0tG14=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144 h1:gwy5JzP6+PhcPFG1obkUSLGcTkUY88sLKlCPOFjwtak=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
|
||||
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=
|
||||
|
||||
@@ -101,7 +101,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commitMessage": tr.CommitSummaryTitle,
|
||||
"commitDescription": tr.CommitDescriptionTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
@@ -116,7 +116,6 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
"worktrees": tr.WorktreesTitle,
|
||||
}
|
||||
|
||||
title, ok := contextTitleMap[str]
|
||||
|
||||
@@ -37,8 +37,6 @@ type GitCommand struct {
|
||||
Tag *git_commands.TagCommands
|
||||
WorkingTree *git_commands.WorkingTreeCommands
|
||||
Bisect *git_commands.BisectCommands
|
||||
Worktree *git_commands.WorktreeCommands
|
||||
Version *git_commands.GitVersion
|
||||
|
||||
Loaders Loaders
|
||||
}
|
||||
@@ -52,7 +50,6 @@ type Loaders struct {
|
||||
RemoteLoader *git_commands.RemoteLoader
|
||||
StashLoader *git_commands.StashLoader
|
||||
TagLoader *git_commands.TagLoader
|
||||
Worktrees *git_commands.WorktreeLoader
|
||||
}
|
||||
|
||||
func NewGitCommand(
|
||||
@@ -130,14 +127,12 @@ func NewGitCommandAux(
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
@@ -159,8 +154,6 @@ func NewGitCommandAux(
|
||||
Tag: tagCommands,
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Worktree: worktreeCommands,
|
||||
Version: version,
|
||||
Loaders: Loaders{
|
||||
BranchLoader: branchLoader,
|
||||
CommitFileLoader: commitFileLoader,
|
||||
@@ -168,7 +161,6 @@ func NewGitCommandAux(
|
||||
FileLoader: fileLoader,
|
||||
ReflogCommitLoader: reflogCommitLoader,
|
||||
RemoteLoader: remoteLoader,
|
||||
Worktrees: worktreeLoader,
|
||||
StashLoader: stashLoader,
|
||||
TagLoader: tagLoader,
|
||||
},
|
||||
|
||||
@@ -50,13 +50,13 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
|
||||
Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
func (self *CommitCommands) CommitCmdObj(summary string, description string) oscommands.ICmdObj {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
|
||||
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
|
||||
ArgIf(skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix), "--no-verify").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
Arg(messageArgs...).
|
||||
ToArgv()
|
||||
@@ -69,8 +69,8 @@ func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
func (self *CommitCommands) RewordLastCommit(summary string, description string) error {
|
||||
messageArgs := self.commitMessageArgs(summary, description)
|
||||
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
Arg("--allow-empty", "--amend", "--only").
|
||||
@@ -80,9 +80,8 @@ func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) commitMessageArgs(message string) []string {
|
||||
msg, description, _ := strings.Cut(message, "\n")
|
||||
args := []string{"-m", msg}
|
||||
func (self *CommitCommands) commitMessageArgs(summary string, description string) []string {
|
||||
args := []string{"-m", summary}
|
||||
|
||||
if description != "" {
|
||||
args = append(args, "-m", description)
|
||||
|
||||
@@ -10,20 +10,23 @@ import (
|
||||
|
||||
func TestCommitRewordCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
input string
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
summary string
|
||||
description string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Single line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
|
||||
"test",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Multi line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
|
||||
"test\nline 2\nline 3",
|
||||
"test",
|
||||
"line 2\nline 3",
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
@@ -31,7 +34,7 @@ func TestCommitRewordCommit(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
assert.NoError(t, instance.RewordLastCommit(s.input))
|
||||
assert.NoError(t, instance.RewordLastCommit(s.summary, s.description))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
@@ -50,7 +53,8 @@ func TestCommitResetToCommit(t *testing.T) {
|
||||
func TestCommitCommitCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
summary string
|
||||
description string
|
||||
configSignoff bool
|
||||
configSkipHookPrefix string
|
||||
expectedArgs []string
|
||||
@@ -59,35 +63,36 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
summary: "test",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "",
|
||||
expectedArgs: []string{"commit", "-m", "test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "WIP: test",
|
||||
summary: "WIP: test",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "WIP",
|
||||
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
summary: "line1",
|
||||
description: "line2",
|
||||
configSignoff: false,
|
||||
configSkipHookPrefix: "",
|
||||
expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with signoff",
|
||||
message: "test",
|
||||
summary: "test",
|
||||
configSignoff: true,
|
||||
configSkipHookPrefix: "",
|
||||
expectedArgs: []string{"commit", "--signoff", "-m", "test"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with signoff and no-verify",
|
||||
message: "WIP: test",
|
||||
summary: "WIP: test",
|
||||
configSignoff: true,
|
||||
configSkipHookPrefix: "WIP",
|
||||
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"},
|
||||
@@ -104,7 +109,7 @@ func TestCommitCommitCmdObj(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
||||
|
||||
assert.NoError(t, instance.CommitCmdObj(s.message).Run())
|
||||
assert.NoError(t, instance.CommitCmdObj(s.summary, s.description).Run())
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -49,13 +49,6 @@ func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) WorktreePath(path string) *GitCommandBuilder {
|
||||
// worktree path comes before the command
|
||||
self.args = append([]string{"--work-tree", path}, self.args...)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ToArgv() []string {
|
||||
return append([]string{"git"}, self.args...)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
|
||||
head_message, _ := self.commit.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
|
||||
if err := self.commit.CommitCmdObj(new_message, "").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ func NewRebaseCommands(
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return self.commit.RewordLastCommit(message)
|
||||
return self.commit.RewordLastCommit(summary, description)
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
|
||||
@@ -47,7 +47,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
|
||||
}
|
||||
|
||||
// now the selected commit should be our head so we'll amend it with the new message
|
||||
err = self.commit.RewordLastCommit(message)
|
||||
err = self.commit.RewordLastCommit(summary, description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -69,7 +69,3 @@ func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
|
||||
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
|
||||
func (v *GitVersion) SupportsWorktrees() bool {
|
||||
return !v.IsOlderThan(2, 5, 0)
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type WorktreeCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
|
||||
func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands {
|
||||
return &WorktreeCommands{
|
||||
GitCommon: gitCommon,
|
||||
}
|
||||
}
|
||||
|
||||
type NewWorktreeOpts struct {
|
||||
// required. The path of the new worktree.
|
||||
Path string
|
||||
// required. The base branch/ref.
|
||||
Base string
|
||||
|
||||
// if true, ends up with a detached head
|
||||
Detach bool
|
||||
|
||||
// optional. if empty, and if detach is false, we will checkout the base
|
||||
Branch string
|
||||
}
|
||||
|
||||
func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
|
||||
if opts.Detach && opts.Branch != "" {
|
||||
panic("cannot specify branch when detaching")
|
||||
}
|
||||
|
||||
cmdArgs := NewGitCmd("worktree").Arg("add").
|
||||
ArgIf(opts.Detach, "--detach").
|
||||
ArgIf(opts.Branch != "", "-b", opts.Branch).
|
||||
Arg(opts.Path, opts.Base)
|
||||
|
||||
return self.cmd.New(cmdArgs.ToArgv()).Run()
|
||||
}
|
||||
|
||||
func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
|
||||
cmdArgs := NewGitCmd("worktree").Arg("remove").ArgIf(force, "-f").Arg(worktreePath).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
func (self *WorktreeCommands) Detach(worktreePath string) error {
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--detach").ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).SetWd(worktreePath).Run()
|
||||
}
|
||||
|
||||
func (self *WorktreeCommands) IsCurrentWorktree(path string) bool {
|
||||
return IsCurrentWorktree(path)
|
||||
}
|
||||
|
||||
func IsCurrentWorktree(path string) bool {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
return EqualPath(pwd, path)
|
||||
}
|
||||
|
||||
func (self *WorktreeCommands) IsWorktreePathMissing(path string) bool {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", path, err).Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checks if two paths are equal
|
||||
// TODO: support relative paths
|
||||
func EqualPath(a string, b string) bool {
|
||||
return a == b
|
||||
}
|
||||
|
||||
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
|
||||
for _, worktree := range worktrees {
|
||||
if worktree.Branch == branch.Name {
|
||||
return worktree, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktree) bool {
|
||||
worktree, ok := WorktreeForBranch(branch, worktrees)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return !IsCurrentWorktree(worktree.Path)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type WorktreeLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewWorktreeLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *WorktreeLoader {
|
||||
return &WorktreeLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
||||
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain", "-z").ToArgv()
|
||||
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(worktreesOutput, "\x00")
|
||||
|
||||
var worktrees []*models.Worktree
|
||||
var current *models.Worktree
|
||||
for _, splitLine := range splitLines {
|
||||
if len(splitLine) == 0 && current != nil {
|
||||
worktrees = append(worktrees, current)
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(splitLine, "worktree ") {
|
||||
path := strings.SplitN(splitLine, " ", 2)[1]
|
||||
current = &models.Worktree{
|
||||
IsMain: len(worktrees) == 0,
|
||||
Path: path,
|
||||
}
|
||||
} else if strings.HasPrefix(splitLine, "branch ") {
|
||||
branch := strings.SplitN(splitLine, " ", 2)[1]
|
||||
current.Branch = strings.TrimPrefix(branch, "refs/heads/")
|
||||
}
|
||||
}
|
||||
|
||||
names := getUniqueNamesFromPaths(lo.Map(worktrees, func(worktree *models.Worktree, _ int) string {
|
||||
return worktree.Path
|
||||
}))
|
||||
|
||||
for index, worktree := range worktrees {
|
||||
worktree.NameField = names[index]
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// move current worktree to the top
|
||||
for i, worktree := range worktrees {
|
||||
if EqualPath(worktree.Path, pwd) {
|
||||
worktrees = append(worktrees[:i], worktrees[i+1:]...)
|
||||
worktrees = append([]*models.Worktree{worktree}, worktrees...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Some worktrees are on a branch but are mid-rebase, and in those cases,
|
||||
// `git worktree list` will not show the branch name. We can get the branch
|
||||
// name from the `rebase-merge/head-name` file (if it exists) in the folder
|
||||
// for the worktree in the parent repo's .git/worktrees folder.
|
||||
for _, worktree := range worktrees {
|
||||
// No point checking if we already have a branch name
|
||||
if worktree.Branch != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rebaseBranch, ok := rebaseBranch(worktree.Path)
|
||||
if ok {
|
||||
worktree.Branch = rebaseBranch
|
||||
}
|
||||
}
|
||||
|
||||
return worktrees, nil
|
||||
}
|
||||
|
||||
func rebaseBranch(worktreePath string) (string, bool) {
|
||||
// need to find the actual path of the worktree in the .git dir
|
||||
gitPath, ok := WorktreeGitPath(worktreePath)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// now we look inside that git path for a file `rebase-merge/head-name`
|
||||
// if it exists, we update the worktree to say that it has that for a head
|
||||
headNameContents, err := os.ReadFile(filepath.Join(gitPath, "rebase-merge", "head-name"))
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
headName := strings.TrimSpace(string(headNameContents))
|
||||
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
|
||||
|
||||
return shortHeadName, true
|
||||
}
|
||||
|
||||
func WorktreeGitPath(worktreePath string) (string, bool) {
|
||||
// first we get the path of the worktree, then we look at the contents of the `.git` file in that path
|
||||
// then we look for the line that says `gitdir: /path/to/.git/worktrees/<worktree-name>`
|
||||
// then we return that path
|
||||
gitFileContents, err := os.ReadFile(filepath.Join(worktreePath, ".git"))
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
gitDirLine := lo.Filter(strings.Split(string(gitFileContents), "\n"), func(line string, _ int) bool {
|
||||
return strings.HasPrefix(line, "gitdir: ")
|
||||
})
|
||||
|
||||
if len(gitDirLine) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
gitDir := strings.TrimPrefix(gitDirLine[0], "gitdir: ")
|
||||
return gitDir, true
|
||||
}
|
||||
|
||||
type pathWithIndexT struct {
|
||||
path string
|
||||
index int
|
||||
}
|
||||
|
||||
type nameWithIndexT struct {
|
||||
name string
|
||||
index int
|
||||
}
|
||||
|
||||
func getUniqueNamesFromPaths(paths []string) []string {
|
||||
pathsWithIndex := lo.Map(paths, func(path string, index int) pathWithIndexT {
|
||||
return pathWithIndexT{path, index}
|
||||
})
|
||||
|
||||
namesWithIndex := getUniqueNamesFromPathsAux(pathsWithIndex, 0)
|
||||
|
||||
// now sort based on index
|
||||
result := make([]string, len(namesWithIndex))
|
||||
for _, nameWithIndex := range namesWithIndex {
|
||||
result[nameWithIndex.index] = nameWithIndex.name
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getUniqueNamesFromPathsAux(paths []pathWithIndexT, depth int) []nameWithIndexT {
|
||||
// If we have no paths, return an empty array
|
||||
if len(paths) == 0 {
|
||||
return []nameWithIndexT{}
|
||||
}
|
||||
|
||||
// If we have only one path, return the last segment of the path
|
||||
if len(paths) == 1 {
|
||||
path := paths[0]
|
||||
return []nameWithIndexT{{index: path.index, name: sliceAtDepth(path.path, depth)}}
|
||||
}
|
||||
|
||||
// group the paths by their value at the specified depth
|
||||
groups := make(map[string][]pathWithIndexT)
|
||||
for _, path := range paths {
|
||||
value := valueAtDepth(path.path, depth)
|
||||
groups[value] = append(groups[value], path)
|
||||
}
|
||||
|
||||
result := []nameWithIndexT{}
|
||||
for _, group := range groups {
|
||||
if len(group) == 1 {
|
||||
path := group[0]
|
||||
result = append(result, nameWithIndexT{index: path.index, name: sliceAtDepth(path.path, depth)})
|
||||
} else {
|
||||
result = append(result, getUniqueNamesFromPathsAux(group, depth+1)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'c', etc
|
||||
func valueAtDepth(path string, depth int) string {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
|
||||
// Split the path into segments
|
||||
segments := strings.Split(path, "/")
|
||||
|
||||
// Get the length of segments
|
||||
length := len(segments)
|
||||
|
||||
// If the depth is greater than the length of segments, return an empty string
|
||||
if depth >= length {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Return the segment at the specified depth from the end of the path
|
||||
return segments[length-1-depth]
|
||||
}
|
||||
|
||||
// if the path is /a/b/c/d, and the depth is 0, the value is 'd'. If the depth is 1, the value is 'b/c', etc
|
||||
func sliceAtDepth(path string, depth int) string {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
|
||||
// Split the path into segments
|
||||
segments := strings.Split(path, "/")
|
||||
|
||||
// Get the length of segments
|
||||
length := len(segments)
|
||||
|
||||
// If the depth is greater than or equal to the length of segments, return an empty string
|
||||
if depth >= length {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Join the segments from the specified depth till end of the path
|
||||
return strings.Join(segments[length-1-depth:], "/")
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetUniqueNamesFromPaths(t *testing.T) {
|
||||
for _, scenario := range []struct {
|
||||
input []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
input: []string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
input: []string{
|
||||
"/my/path/feature/one",
|
||||
},
|
||||
expected: []string{
|
||||
"one",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []string{
|
||||
"/my/path/feature/one/",
|
||||
},
|
||||
expected: []string{
|
||||
"one",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []string{
|
||||
"/a/b/c/d",
|
||||
"/a/b/c/e",
|
||||
"/a/b/f/d",
|
||||
"/a/e/c/d",
|
||||
},
|
||||
expected: []string{
|
||||
"b/c/d",
|
||||
"e",
|
||||
"f/d",
|
||||
"e/c/d",
|
||||
},
|
||||
},
|
||||
} {
|
||||
actual := getUniqueNamesFromPaths(scenario.input)
|
||||
assert.EqualValues(t, scenario.expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// A commit author
|
||||
type Author struct {
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
|
||||
func (self *Author) Combined() string {
|
||||
return fmt.Sprintf("%s <%s>", self.Name, self.Email)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package models
|
||||
|
||||
// A git worktree
|
||||
type Worktree struct {
|
||||
// if false, this is a linked worktree
|
||||
IsMain bool
|
||||
Path string
|
||||
Branch string
|
||||
// based on the path, but uniquified
|
||||
NameField string
|
||||
}
|
||||
|
||||
func (w *Worktree) RefName() string {
|
||||
return w.Name()
|
||||
}
|
||||
|
||||
func (w *Worktree) ID() string {
|
||||
return w.Path
|
||||
}
|
||||
|
||||
func (w *Worktree) Description() string {
|
||||
return w.RefName()
|
||||
}
|
||||
|
||||
func (w *Worktree) Name() string {
|
||||
return w.NameField
|
||||
}
|
||||
|
||||
func (w *Worktree) Main() bool {
|
||||
return w.IsMain
|
||||
}
|
||||
@@ -24,9 +24,6 @@ type ICmdObj interface {
|
||||
AddEnvVars(...string) ICmdObj
|
||||
GetEnvVars() []string
|
||||
|
||||
// sets the working directory
|
||||
SetWd(string) ICmdObj
|
||||
|
||||
// runs the command and returns an error if any
|
||||
Run() error
|
||||
// runs the command and returns the output as a string, and an error if any
|
||||
@@ -145,12 +142,6 @@ func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
||||
func (self *CmdObj) SetWd(wd string) ICmdObj {
|
||||
self.cmd.Dir = wd
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) DontLog() ICmdObj {
|
||||
self.dontLog = true
|
||||
return self
|
||||
|
||||
@@ -331,9 +331,13 @@ func (self *cmdObjRunner) processOutput(
|
||||
askFor, ok := checkForCredentialRequest(newBytes)
|
||||
if ok {
|
||||
responseChan := promptUserForCredential(askFor)
|
||||
task.Pause()
|
||||
if task != nil {
|
||||
task.Pause()
|
||||
}
|
||||
toInput := <-responseChan
|
||||
task.Continue()
|
||||
if task != nil {
|
||||
task.Continue()
|
||||
}
|
||||
// If the return data is empty we don't write anything to stdin
|
||||
if toInput != "" {
|
||||
_, _ = writer.Write([]byte(toInput))
|
||||
|
||||
@@ -132,7 +132,6 @@ type KeybindingConfig struct {
|
||||
Status KeybindingStatusConfig `yaml:"status"`
|
||||
Files KeybindingFilesConfig `yaml:"files"`
|
||||
Branches KeybindingBranchesConfig `yaml:"branches"`
|
||||
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
|
||||
Commits KeybindingCommitsConfig `yaml:"commits"`
|
||||
Stash KeybindingStashConfig `yaml:"stash"`
|
||||
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
|
||||
@@ -247,10 +246,6 @@ type KeybindingBranchesConfig struct {
|
||||
FetchRemote string `yaml:"fetchRemote"`
|
||||
}
|
||||
|
||||
type KeybindingWorktreesConfig struct {
|
||||
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
|
||||
}
|
||||
|
||||
type KeybindingCommitsConfig struct {
|
||||
SquashDown string `yaml:"squashDown"`
|
||||
RenameCommit string `yaml:"renameCommit"`
|
||||
@@ -589,9 +584,6 @@ func GetDefaultConfig() *UserConfig {
|
||||
SetUpstream: "u",
|
||||
FetchRemote: "f",
|
||||
},
|
||||
Worktrees: KeybindingWorktreesConfig{
|
||||
ViewWorktreeOptions: "w",
|
||||
},
|
||||
Commits: KeybindingCommitsConfig{
|
||||
SquashDown: "s",
|
||||
RenameCommit: "r",
|
||||
|
||||
@@ -376,16 +376,3 @@ func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
|
||||
|
||||
return listContexts
|
||||
}
|
||||
|
||||
func (self *ContextMgr) ContextForKey(key types.ContextKey) types.Context {
|
||||
self.RLock()
|
||||
defer self.RUnlock()
|
||||
|
||||
for _, context := range self.allContexts.Flatten() {
|
||||
if context.GetKey() == key {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
||||
c.Modes().Diffing.Ref,
|
||||
c.Tr,
|
||||
c.UserConfig,
|
||||
c.Model().Worktrees,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ type CommitMessageViewModel struct {
|
||||
// the full preserved message (combined summary and description)
|
||||
preservedMessage string
|
||||
// invoked when pressing enter in the commit message panel
|
||||
onConfirm func(string) error
|
||||
onConfirm func(string, string) error
|
||||
|
||||
// The message typed in before cycling through history
|
||||
// We store this separately to 'preservedMessage' because 'preservedMessage'
|
||||
@@ -88,15 +88,22 @@ func (self *CommitMessageContext) SetHistoryMessage(message string) {
|
||||
self.viewModel.historyMessage = message
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) OnConfirm(message string) error {
|
||||
return self.viewModel.onConfirm(message)
|
||||
func (self *CommitMessageContext) OnConfirm(summary string, description string) error {
|
||||
return self.viewModel.onConfirm(summary, description)
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) SetPanelState(index int, title string, preserveMessage bool, onConfirm func(string) error) {
|
||||
func (self *CommitMessageContext) SetPanelState(
|
||||
index int,
|
||||
summaryTitle string,
|
||||
descriptionTitle string,
|
||||
preserveMessage bool,
|
||||
onConfirm func(string, string) error,
|
||||
) {
|
||||
self.viewModel.selectedindex = index
|
||||
self.viewModel.preserveMessage = preserveMessage
|
||||
self.viewModel.onConfirm = onConfirm
|
||||
self.GetView().Title = title
|
||||
self.GetView().Title = summaryTitle
|
||||
self.c.Views().CommitDescription.Title = descriptionTitle
|
||||
}
|
||||
|
||||
func (self *CommitMessageContext) RenderCommitLength() {
|
||||
|
||||
@@ -5,16 +5,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// used as a nil value when passing a context key as an arg
|
||||
NO_CONTEXT types.ContextKey = "none"
|
||||
|
||||
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
|
||||
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
||||
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
||||
FILES_CONTEXT_KEY types.ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
||||
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
||||
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
||||
@@ -53,7 +49,6 @@ var AllContextKeys = []types.ContextKey{
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
WORKTREES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
LOCAL_COMMITS_CONTEXT_KEY,
|
||||
@@ -89,7 +84,6 @@ type ContextTree struct {
|
||||
LocalCommits *LocalCommitsContext
|
||||
CommitFiles *CommitFilesContext
|
||||
Remotes *RemotesContext
|
||||
Worktrees *WorktreesContext
|
||||
Submodules *SubmodulesContext
|
||||
RemoteBranches *RemoteBranchesContext
|
||||
ReflogCommits *ReflogCommitsContext
|
||||
@@ -127,7 +121,6 @@ func (self *ContextTree) Flatten() []types.Context {
|
||||
self.Files,
|
||||
self.SubCommits,
|
||||
self.Remotes,
|
||||
self.Worktrees,
|
||||
self.RemoteBranches,
|
||||
self.Tags,
|
||||
self.Branches,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sahilm/fuzzy"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
@@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int {
|
||||
return len(self.getList())
|
||||
}
|
||||
|
||||
type fuzzySource[T any] struct {
|
||||
list []T
|
||||
getFilterFields func(T) []string
|
||||
}
|
||||
|
||||
var _ fuzzy.Source = &fuzzySource[string]{}
|
||||
|
||||
func (self *fuzzySource[T]) String(i int) string {
|
||||
return strings.Join(self.getFilterFields(self.list[i]), " ")
|
||||
}
|
||||
|
||||
func (self *fuzzySource[T]) Len() int {
|
||||
return len(self.list)
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) applyFilter() {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
@@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() {
|
||||
if self.filter == "" {
|
||||
self.filteredIndices = nil
|
||||
} else {
|
||||
self.filteredIndices = []int{}
|
||||
for i, item := range self.getList() {
|
||||
for _, field := range self.getFilterFields(item) {
|
||||
if self.match(field, self.filter) {
|
||||
self.filteredIndices = append(self.filteredIndices, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
source := &fuzzySource[T]{
|
||||
list: self.getList(),
|
||||
getFilterFields: self.getFilterFields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) match(haystack string, needle string) bool {
|
||||
return utils.CaseAwareContains(haystack, needle)
|
||||
matches := fuzzy.FindFrom(self.filter, source)
|
||||
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
|
||||
return match.Index
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
|
||||
|
||||
@@ -14,7 +14,7 @@ type ListContextTrait struct {
|
||||
list types.IList
|
||||
getDisplayStrings func(startIdx int, length int) [][]string
|
||||
// Alignment for each column. If nil, the default is left alignment
|
||||
columnAlignments []utils.Alignment
|
||||
getColumnAlignments func() []utils.Alignment
|
||||
// Some contexts, like the commit context, will highlight the path from the selected commit
|
||||
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
|
||||
// so that we show the highlighted path.
|
||||
@@ -31,7 +31,14 @@ func (self *ListContextTrait) GetList() types.IList {
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) FocusLine() {
|
||||
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
|
||||
// Doing this at the end of the layout function because we need the view to be
|
||||
// resized before we focus the line, otherwise if we're in accordion mode
|
||||
// the view could be squashed and won't how to adjust the cursor/origin
|
||||
self.c.AfterLayout(func() error {
|
||||
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
|
||||
return nil
|
||||
})
|
||||
|
||||
self.setFooter()
|
||||
|
||||
if self.refreshViewportOnChange {
|
||||
@@ -75,9 +82,13 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
|
||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||
func (self *ListContextTrait) HandleRender() error {
|
||||
self.list.RefreshSelectedIdx()
|
||||
var columnAlignments []utils.Alignment
|
||||
if self.getColumnAlignments != nil {
|
||||
columnAlignments = self.getColumnAlignments()
|
||||
}
|
||||
content := utils.RenderDisplayStrings(
|
||||
self.getDisplayStrings(0, self.list.Len()),
|
||||
self.columnAlignments,
|
||||
columnAlignments,
|
||||
)
|
||||
self.GetViewTrait().SetContent(content)
|
||||
self.c.Render()
|
||||
|
||||
@@ -35,10 +35,10 @@ func NewMenuContext(
|
||||
Focusable: true,
|
||||
HasUncontrolledBounds: true,
|
||||
})),
|
||||
getDisplayStrings: viewModel.GetDisplayStrings,
|
||||
list: viewModel,
|
||||
c: c,
|
||||
columnAlignments: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
|
||||
getDisplayStrings: viewModel.GetDisplayStrings,
|
||||
list: viewModel,
|
||||
c: c,
|
||||
getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,9 @@ func (self *MenuContext) GetSelectedItemId() string {
|
||||
}
|
||||
|
||||
type MenuViewModel struct {
|
||||
c *ContextCommon
|
||||
menuItems []*types.MenuItem
|
||||
c *ContextCommon
|
||||
menuItems []*types.MenuItem
|
||||
columnAlignment []utils.Alignment
|
||||
*FilteredListViewModel[*types.MenuItem]
|
||||
}
|
||||
|
||||
@@ -73,8 +74,9 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) {
|
||||
func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment []utils.Alignment) {
|
||||
self.menuItems = items
|
||||
self.columnAlignment = columnAlignment
|
||||
}
|
||||
|
||||
// TODO: move into presentation package
|
||||
@@ -135,6 +137,10 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if selectedItem == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := selectedItem.OnPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
||||
Submodules: NewSubmodulesContext(c),
|
||||
Menu: NewMenuContext(c),
|
||||
Remotes: NewRemotesContext(c),
|
||||
Worktrees: NewWorktreesContext(c),
|
||||
RemoteBranches: NewRemoteBranchesContext(c),
|
||||
LocalCommits: NewLocalCommitsContext(c),
|
||||
CommitFiles: commitFilesContext,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type WorktreesContext struct {
|
||||
*FilteredListViewModel[*models.Worktree]
|
||||
*ListContextTrait
|
||||
}
|
||||
|
||||
var _ types.IListContext = (*WorktreesContext)(nil)
|
||||
|
||||
func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
|
||||
viewModel := NewFilteredListViewModel(
|
||||
func() []*models.Worktree { return c.Model().Worktrees },
|
||||
func(Worktree *models.Worktree) []string {
|
||||
return []string{Worktree.Name()}
|
||||
},
|
||||
)
|
||||
|
||||
getDisplayStrings := func(startIdx int, length int) [][]string {
|
||||
return presentation.GetWorktreeDisplayStrings(
|
||||
viewModel.GetFilteredList(),
|
||||
c.Git().Worktree.IsCurrentWorktree,
|
||||
c.Git().Worktree.IsWorktreePathMissing,
|
||||
)
|
||||
}
|
||||
|
||||
return &WorktreesContext{
|
||||
FilteredListViewModel: viewModel,
|
||||
ListContextTrait: &ListContextTrait{
|
||||
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||
View: c.Views().Worktrees,
|
||||
WindowName: "branches",
|
||||
Key: WORKTREES_CONTEXT_KEY,
|
||||
Kind: types.SIDE_CONTEXT,
|
||||
Focusable: true,
|
||||
})),
|
||||
list: viewModel,
|
||||
getDisplayStrings: getDisplayStrings,
|
||||
c: c,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreesContext) GetSelectedItemId() string {
|
||||
item := self.GetSelected()
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return item.ID()
|
||||
}
|
||||
+6
-39
@@ -18,13 +18,10 @@ func (gui *Gui) Helpers() *helpers.Helpers {
|
||||
|
||||
func (gui *Gui) resetHelpersAndControllers() {
|
||||
helperCommon := gui.c
|
||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
|
||||
refsHelper := helpers.NewRefsHelper(helperCommon)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper, refsHelper, suggestionsHelper)
|
||||
|
||||
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
|
||||
suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon)
|
||||
|
||||
setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage })
|
||||
setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription })
|
||||
@@ -44,20 +41,11 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
|
||||
gpgHelper := helpers.NewGpgHelper(helperCommon)
|
||||
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
|
||||
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
|
||||
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
|
||||
stagingHelper := helpers.NewStagingHelper(helperCommon)
|
||||
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
|
||||
|
||||
refreshHelper := helpers.NewRefreshHelper(
|
||||
helperCommon,
|
||||
refsHelper,
|
||||
rebaseHelper,
|
||||
patchBuildingHelper,
|
||||
stagingHelper,
|
||||
mergeConflictsHelper,
|
||||
worktreeHelper,
|
||||
gui.fileWatcher,
|
||||
)
|
||||
refreshHelper := helpers.NewRefreshHelper(helperCommon, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher)
|
||||
diffHelper := helpers.NewDiffHelper(helperCommon)
|
||||
cherryPickHelper := helpers.NewCherryPickHelper(
|
||||
helperCommon,
|
||||
@@ -86,7 +74,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
Suggestions: suggestionsHelper,
|
||||
Files: helpers.NewFilesHelper(helperCommon),
|
||||
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
|
||||
Tags: helpers.NewTagsHelper(helperCommon),
|
||||
Tags: helpers.NewTagsHelper(helperCommon, commitsHelper),
|
||||
GPG: helpers.NewGpgHelper(helperCommon),
|
||||
MergeAndRebase: rebaseHelper,
|
||||
MergeConflicts: mergeConflictsHelper,
|
||||
@@ -96,7 +84,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
Commits: commitsHelper,
|
||||
Snake: helpers.NewSnakeHelper(helperCommon),
|
||||
Diff: diffHelper,
|
||||
Repos: reposHelper,
|
||||
Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo),
|
||||
RecordDirectory: recordDirectoryHelper,
|
||||
Update: helpers.NewUpdateHelper(helperCommon, gui.Updater),
|
||||
Window: windowHelper,
|
||||
@@ -111,8 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
modeHelper,
|
||||
appStatusHelper,
|
||||
),
|
||||
Search: helpers.NewSearchHelper(helperCommon),
|
||||
Worktree: worktreeHelper,
|
||||
Search: helpers.NewSearchHelper(helperCommon),
|
||||
}
|
||||
|
||||
gui.CustomCommandsClient = custom_commands.NewClient(
|
||||
@@ -151,7 +138,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
common,
|
||||
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
|
||||
)
|
||||
worktreesController := controllers.NewWorktreesController(common)
|
||||
undoController := controllers.NewUndoController(common)
|
||||
globalController := controllers.NewGlobalController(common)
|
||||
contextLinesController := controllers.NewContextLinesController(common)
|
||||
@@ -191,7 +177,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
for _, context := range []types.Context{
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.Worktrees,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
@@ -242,20 +227,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
|
||||
}
|
||||
|
||||
if gui.c.Git().Version.SupportsWorktrees() {
|
||||
for _, context := range []controllers.CanViewWorktreeOptions{
|
||||
gui.State.Contexts.LocalCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
} {
|
||||
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
|
||||
}
|
||||
}
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
|
||||
reflogCommitsController,
|
||||
)
|
||||
@@ -327,10 +298,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
remotesController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Worktrees,
|
||||
worktreesController,
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.Stash,
|
||||
stashController,
|
||||
)
|
||||
|
||||
@@ -202,29 +202,10 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
||||
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
|
||||
}
|
||||
|
||||
worktreeForRef, ok := self.worktreeForBranch(selectedBranch)
|
||||
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef.Path) {
|
||||
return self.promptToCheckoutWorktree(worktreeForRef)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
|
||||
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
||||
}
|
||||
|
||||
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
|
||||
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
|
||||
}
|
||||
|
||||
func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktree) error {
|
||||
return self.c.Confirm(types.ConfirmOpts{
|
||||
Title: "Switch to worktree",
|
||||
Prompt: fmt.Sprintf("This branch is checked out by worktree %s. Do you want to switch to that worktree?", worktree.Name()),
|
||||
HandleConfirm: func() error {
|
||||
return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
|
||||
return self.createPullRequest(selectedBranch.Name, "")
|
||||
}
|
||||
@@ -317,51 +298,9 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
||||
if checkedOutBranch.Name == branch.Name {
|
||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
||||
}
|
||||
|
||||
if self.checkedOutByOtherWorktree(branch) {
|
||||
return self.promptWorktreeBranchDelete(branch)
|
||||
}
|
||||
|
||||
return self.deleteWithForce(branch, false)
|
||||
}
|
||||
|
||||
func (self *BranchesController) checkedOutByOtherWorktree(branch *models.Branch) bool {
|
||||
return git_commands.CheckedOutByOtherWorktree(branch, self.c.Model().Worktrees)
|
||||
}
|
||||
|
||||
func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
|
||||
worktree, ok := self.worktreeForBranch(selectedBranch)
|
||||
if !ok {
|
||||
self.c.Log.Error("CheckedOutByOtherWorktree out of sync with list of worktrees")
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: fmt.Sprintf("Branch %s is checked out by worktree %s", selectedBranch.Name, worktree.Name()),
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: "Switch to worktree",
|
||||
OnPress: func() error {
|
||||
return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Detach worktree",
|
||||
Tooltip: "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",
|
||||
OnPress: func() error {
|
||||
return self.c.Helpers().Worktree.Detach(worktree)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Remove worktree",
|
||||
OnPress: func() error {
|
||||
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
|
||||
title := self.c.Tr.DeleteBranch
|
||||
var templateStr string
|
||||
@@ -456,7 +395,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
|
||||
}
|
||||
|
||||
func (self *BranchesController) createTag(branch *models.Branch) error {
|
||||
return self.c.Helpers().Tags.CreateTagMenu(branch.FullRefName(), func() {})
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt(branch.FullRefName(), func() {})
|
||||
}
|
||||
|
||||
func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
|
||||
|
||||
@@ -51,7 +51,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||
},
|
||||
Key: 'x',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
@@ -72,7 +72,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||
},
|
||||
Key: 'u',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
@@ -107,7 +107,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||
},
|
||||
Key: 'x',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
@@ -128,7 +128,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES, types.WORKTREES}})
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
|
||||
},
|
||||
Key: 'u',
|
||||
Tooltip: utils.ResolvePlaceholderString(
|
||||
|
||||
@@ -63,33 +63,44 @@ func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) UpdateCommitPanelView(message string) {
|
||||
// first try the passed in message, if not fallback to context -> view in that order
|
||||
if message != "" {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
return
|
||||
}
|
||||
message = self.c.Contexts().CommitMessage.GetPreservedMessage()
|
||||
if message != "" {
|
||||
self.SetMessageAndDescriptionInView(message)
|
||||
} else {
|
||||
self.SetMessageAndDescriptionInView(self.getCommitSummary())
|
||||
|
||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||
preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessage()
|
||||
self.SetMessageAndDescriptionInView(preservedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
self.SetMessageAndDescriptionInView("")
|
||||
}
|
||||
|
||||
type OpenCommitMessagePanelOpts struct {
|
||||
CommitIndex int
|
||||
Title string
|
||||
PreserveMessage bool
|
||||
OnConfirm func(string) error
|
||||
InitialMessage string
|
||||
CommitIndex int
|
||||
SummaryTitle string
|
||||
DescriptionTitle string
|
||||
PreserveMessage bool
|
||||
OnConfirm func(summary string, description string) error
|
||||
InitialMessage string
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error {
|
||||
onConfirm := func(summary string, description string) error {
|
||||
if err := self.CloseCommitMessagePanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return opts.OnConfirm(summary, description)
|
||||
}
|
||||
|
||||
self.c.Contexts().CommitMessage.SetPanelState(
|
||||
opts.CommitIndex,
|
||||
opts.Title,
|
||||
opts.SummaryTitle,
|
||||
opts.DescriptionTitle,
|
||||
opts.PreserveMessage,
|
||||
opts.OnConfirm,
|
||||
onConfirm,
|
||||
)
|
||||
|
||||
self.UpdateCommitPanelView(opts.InitialMessage)
|
||||
@@ -102,17 +113,16 @@ func (self *CommitsHelper) OnCommitSuccess() {
|
||||
if self.c.Contexts().CommitMessage.GetPreserveMessage() {
|
||||
self.c.Contexts().CommitMessage.SetPreservedMessage("")
|
||||
}
|
||||
self.SetMessageAndDescriptionInView("")
|
||||
}
|
||||
|
||||
func (self *CommitsHelper) HandleCommitConfirm() error {
|
||||
fullMessage := self.JoinCommitMessageAndDescription()
|
||||
summary, description := self.getCommitSummary(), self.getCommitDescription()
|
||||
|
||||
if fullMessage == "" {
|
||||
if summary == "" {
|
||||
return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
|
||||
err := self.c.Contexts().CommitMessage.OnConfirm(fullMessage)
|
||||
err := self.c.Contexts().CommitMessage.OnConfirm(summary, description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -302,7 +302,12 @@ func (self *ConfirmationHelper) resizeMenu() {
|
||||
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
|
||||
|
||||
tooltipTop := menuBottom + 1
|
||||
tooltipHeight := getMessageHeight(true, self.c.Contexts().Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame
|
||||
tooltip := ""
|
||||
selectedItem := self.c.Contexts().Menu.GetSelected()
|
||||
if selectedItem != nil {
|
||||
tooltip = selectedItem.Tooltip
|
||||
}
|
||||
tooltipHeight := getMessageHeight(true, tooltip, panelWidth) + 2 // plus 2 for the frame
|
||||
_, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ type Helpers struct {
|
||||
AppStatus *AppStatusHelper
|
||||
WindowArrangement *WindowArrangementHelper
|
||||
Search *SearchHelper
|
||||
Worktree *WorktreeHelper
|
||||
}
|
||||
|
||||
func NewStubHelpers() *Helpers {
|
||||
@@ -81,6 +80,5 @@ func NewStubHelpers() *Helpers {
|
||||
AppStatus: &AppStatusHelper{},
|
||||
WindowArrangement: &WindowArrangementHelper{},
|
||||
Search: &SearchHelper{},
|
||||
Worktree: &WorktreeHelper{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -25,7 +27,6 @@ type RefreshHelper struct {
|
||||
patchBuildingHelper *PatchBuildingHelper
|
||||
stagingHelper *StagingHelper
|
||||
mergeConflictsHelper *MergeConflictsHelper
|
||||
worktreeHelper *WorktreeHelper
|
||||
fileWatcher types.IFileWatcher
|
||||
}
|
||||
|
||||
@@ -36,7 +37,6 @@ func NewRefreshHelper(
|
||||
patchBuildingHelper *PatchBuildingHelper,
|
||||
stagingHelper *StagingHelper,
|
||||
mergeConflictsHelper *MergeConflictsHelper,
|
||||
worktreeHelper *WorktreeHelper,
|
||||
fileWatcher types.IFileWatcher,
|
||||
) *RefreshHelper {
|
||||
return &RefreshHelper{
|
||||
@@ -46,7 +46,6 @@ func NewRefreshHelper(
|
||||
patchBuildingHelper: patchBuildingHelper,
|
||||
stagingHelper: stagingHelper,
|
||||
mergeConflictsHelper: mergeConflictsHelper,
|
||||
worktreeHelper: worktreeHelper,
|
||||
fileWatcher: fileWatcher,
|
||||
}
|
||||
}
|
||||
@@ -78,7 +77,6 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
||||
types.REFLOG,
|
||||
types.TAGS,
|
||||
types.REMOTES,
|
||||
types.WORKTREES,
|
||||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
@@ -130,10 +128,6 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
|
||||
refresh(func() { _ = self.refreshRemotes() })
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.WORKTREES) {
|
||||
refresh(func() { _ = self.refreshWorktrees() })
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.STAGING) {
|
||||
refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) })
|
||||
}
|
||||
@@ -176,7 +170,6 @@ func getScopeNames(scopes []types.RefreshableView) []string {
|
||||
types.REFLOG: "reflog",
|
||||
types.TAGS: "tags",
|
||||
types.REMOTES: "remotes",
|
||||
types.WORKTREES: "worktrees",
|
||||
types.STATUS: "status",
|
||||
types.BISECT_INFO: "bisect",
|
||||
types.STAGING: "staging",
|
||||
@@ -273,6 +266,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
|
||||
return err
|
||||
}
|
||||
self.c.Model().Commits = commits
|
||||
self.RefreshAuthors(commits)
|
||||
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
|
||||
|
||||
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
|
||||
@@ -294,10 +288,26 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
|
||||
return err
|
||||
}
|
||||
self.c.Model().SubCommits = commits
|
||||
self.RefreshAuthors(commits)
|
||||
|
||||
return self.c.PostRefreshUpdate(self.c.Contexts().SubCommits)
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) RefreshAuthors(commits []*models.Commit) {
|
||||
self.c.Mutexes().AuthorsMutex.Lock()
|
||||
defer self.c.Mutexes().AuthorsMutex.Unlock()
|
||||
|
||||
authors := self.c.Model().Authors
|
||||
for _, commit := range commits {
|
||||
if _, ok := authors[commit.AuthorEmail]; !ok {
|
||||
authors[commit.AuthorEmail] = &models.Author{
|
||||
Email: commit.AuthorEmail,
|
||||
Name: commit.AuthorName,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshCommitFilesContext() error {
|
||||
ref := self.c.Contexts().CommitFiles.GetRef()
|
||||
to := ref.RefName()
|
||||
@@ -564,22 +574,6 @@ func (self *RefreshHelper) refreshRemotes() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshWorktrees() error {
|
||||
if !self.c.Git().Version.SupportsWorktrees() {
|
||||
self.c.Model().Worktrees = []*models.Worktree{}
|
||||
return nil
|
||||
}
|
||||
|
||||
worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees()
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
self.c.Model().Worktrees = worktrees
|
||||
|
||||
return self.c.PostRefreshUpdate(self.c.Contexts().Worktrees)
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshStashEntries() error {
|
||||
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
|
||||
GetStashEntries(self.c.Modes().Filtering.GetPath())
|
||||
@@ -597,16 +591,20 @@ func (self *RefreshHelper) refreshStatus() {
|
||||
// need to wait for branches to refresh
|
||||
return
|
||||
}
|
||||
status := ""
|
||||
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
var linkedWorktreeName string
|
||||
if self.c.Git().Version.SupportsWorktrees() {
|
||||
linkedWorktreeName = self.worktreeHelper.GetLinkedWorktreeName()
|
||||
if currentBranch.IsRealBranch() {
|
||||
status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " "
|
||||
}
|
||||
|
||||
repoName := self.worktreeHelper.GetCurrentRepoName()
|
||||
workingTreeState := self.c.Git().Status.WorkingTreeState()
|
||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||
status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeStateLower(self.c.Tr, workingTreeState))
|
||||
}
|
||||
|
||||
status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr)
|
||||
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
self.c.SetViewContent(self.c.Views().Status, status)
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool, contextKey types.ContextKey) error
|
||||
type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error
|
||||
|
||||
// helps switch back and forth between repos
|
||||
type ReposHelper struct {
|
||||
@@ -47,7 +46,7 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
|
||||
}
|
||||
self.c.State().GetRepoPathStack().Push(wd)
|
||||
|
||||
return self.DispatchSwitchToRepo(submodule.Path, true, context.NO_CONTEXT)
|
||||
return self.DispatchSwitchToRepo(submodule.Path, true)
|
||||
}
|
||||
|
||||
func (self *ReposHelper) getCurrentBranch(path string) string {
|
||||
@@ -130,7 +129,7 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
||||
// if we were in a submodule, we want to forget about that stack of repos
|
||||
// so that hitting escape in the new repo does nothing
|
||||
self.c.State().GetRepoPathStack().Clear()
|
||||
return self.DispatchSwitchToRepo(path, false, context.NO_CONTEXT)
|
||||
return self.DispatchSwitchToRepo(path, false)
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -138,22 +137,16 @@ func (self *ReposHelper) CreateRecentReposMenu() error {
|
||||
return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems})
|
||||
}
|
||||
|
||||
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool, contextKey types.ContextKey) error {
|
||||
return self.DispatchSwitchTo(path, reuse, self.c.Tr.ErrRepositoryMovedOrDeleted, contextKey)
|
||||
}
|
||||
|
||||
func (self *ReposHelper) DispatchSwitchTo(path string, reuse bool, errMsg string, contextKey types.ContextKey) error {
|
||||
func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error {
|
||||
env.UnsetGitDirEnvs()
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogCommand(fmt.Sprintf("Changing directory to %s", path), false)
|
||||
|
||||
if err := os.Chdir(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return self.c.ErrorMsg(errMsg)
|
||||
return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -178,5 +171,5 @@ func (self *ReposHelper) DispatchSwitchTo(path string, reuse bool, errMsg string
|
||||
self.c.Mutexes().RefreshingFilesMutex.Lock()
|
||||
defer self.c.Mutexes().RefreshingFilesMutex.Unlock()
|
||||
|
||||
return self.onNewRepo(appTypes.StartArgs{}, reuse, contextKey)
|
||||
return self.onNewRepo(appTypes.StartArgs{}, reuse)
|
||||
}
|
||||
|
||||
@@ -176,9 +176,11 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su
|
||||
}
|
||||
|
||||
func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
authors := lo.Uniq(slices.Map(self.c.Model().Commits, func(commit *models.Commit) string {
|
||||
return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail)
|
||||
}))
|
||||
authors := lo.Map(lo.Values(self.c.Model().Authors), func(author *models.Author, _ int) string {
|
||||
return author.Combined()
|
||||
})
|
||||
|
||||
slices.Sort(authors)
|
||||
|
||||
return FuzzySearchFunc(authors)
|
||||
}
|
||||
|
||||
@@ -1,77 +1,54 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// Helper structs are for defining functionality that could be used by multiple contexts.
|
||||
// For example, here we have a CreateTagMenu which is applicable to both the tags context
|
||||
// and the commits context.
|
||||
|
||||
type TagsHelper struct {
|
||||
c *HelperCommon
|
||||
c *HelperCommon
|
||||
commitsHelper *CommitsHelper
|
||||
}
|
||||
|
||||
func NewTagsHelper(c *HelperCommon) *TagsHelper {
|
||||
func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper) *TagsHelper {
|
||||
return &TagsHelper{
|
||||
c: c,
|
||||
c: c,
|
||||
commitsHelper: commitsHelper,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagsHelper) CreateTagMenu(ref string, onCreate func()) error {
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.TagMenuTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.LightweightTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateLightweightTag(ref, onCreate)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.AnnotatedTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateAnnotatedTag(ref, onCreate)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) afterTagCreate(onCreate func()) error {
|
||||
onCreate()
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagMessageTitle,
|
||||
HandleConfirm: func(msg string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, msg); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterTagCreate(onCreate)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func()) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
|
||||
if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
|
||||
return self.c.Error(err)
|
||||
func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error {
|
||||
onConfirm := func(tagName string, description string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.CreatingTag, func(gocui.Task) error {
|
||||
if description != "" {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
} else {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
|
||||
if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
return self.afterTagCreate(onCreate)
|
||||
|
||||
self.commitsHelper.OnCommitSuccess()
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return self.commitsHelper.OpenCommitMessagePanel(
|
||||
&OpenCommitMessagePanelOpts{
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: "",
|
||||
SummaryTitle: self.c.Tr.TagNameTitle,
|
||||
DescriptionTitle: self.c.Tr.TagMessageTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: onConfirm,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,19 +99,19 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
|
||||
|
||||
return self.commitsHelper.OpenCommitMessagePanel(
|
||||
&OpenCommitMessagePanelOpts{
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: initialMessage,
|
||||
Title: self.c.Tr.CommitSummary,
|
||||
PreserveMessage: true,
|
||||
OnConfirm: self.handleCommit,
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: initialMessage,
|
||||
SummaryTitle: self.c.Tr.CommitSummaryTitle,
|
||||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: true,
|
||||
OnConfirm: self.handleCommit,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) handleCommit(message string) error {
|
||||
cmdObj := self.c.Git().Commit.CommitCmdObj(message)
|
||||
func (self *WorkingTreeHelper) handleCommit(summary string, description string) error {
|
||||
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description)
|
||||
self.c.LogAction(self.c.Tr.Actions.Commit)
|
||||
_ = self.commitsHelper.PopCommitMessageContexts()
|
||||
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
|
||||
self.commitsHelper.OnCommitSuccess()
|
||||
return nil
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type IWorktreeHelper interface {
|
||||
GetMainWorktreeName() string
|
||||
GetCurrentWorktreeName() string
|
||||
}
|
||||
|
||||
type WorktreeHelper struct {
|
||||
c *HelperCommon
|
||||
reposHelper *ReposHelper
|
||||
refsHelper *RefsHelper
|
||||
suggestionsHelper *SuggestionsHelper
|
||||
}
|
||||
|
||||
func NewWorktreeHelper(c *HelperCommon, reposHelper *ReposHelper, refsHelper *RefsHelper, suggestionsHelper *SuggestionsHelper) *WorktreeHelper {
|
||||
return &WorktreeHelper{
|
||||
c: c,
|
||||
reposHelper: reposHelper,
|
||||
refsHelper: refsHelper,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) GetMainWorktreeName() string {
|
||||
for _, worktree := range self.c.Model().Worktrees {
|
||||
if worktree.Main() {
|
||||
return worktree.Name()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// If we're on the main worktree, we return an empty string
|
||||
func (self *WorktreeHelper) GetLinkedWorktreeName() string {
|
||||
worktrees := self.c.Model().Worktrees
|
||||
if len(worktrees) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// worktrees always have the current worktree on top
|
||||
currentWorktree := worktrees[0]
|
||||
if currentWorktree.Main() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return currentWorktree.Name()
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) IsCurrentWorktree(w *models.Worktree) bool {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
self.c.Log.Errorf("failed to obtain current working directory: %w", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return pwd == w.Path
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) IsWorktreePathMissing(w *models.Worktree) bool {
|
||||
if _, err := os.Stat(w.Path); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
self.c.Log.Errorf("failed to check if worktree path `%s` exists: %w", w.Path, err)
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) NewWorktree() error {
|
||||
branch := self.refsHelper.GetCheckedOutRef()
|
||||
currentBranchName := branch.RefName()
|
||||
|
||||
f := func(detached bool) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.NewWorktreeBase,
|
||||
InitialContent: currentBranchName,
|
||||
FindSuggestionsFunc: self.suggestionsHelper.GetRefsSuggestionsFunc(),
|
||||
HandleConfirm: func(base string) error {
|
||||
// we assume that the base can be checked out
|
||||
canCheckoutBase := true
|
||||
return self.NewWorktreeCheckout(base, canCheckoutBase, detached)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
placeholders := map[string]string{"ref": "ref"}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.WorktreeTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
|
||||
OnPress: func() error {
|
||||
return f(false)
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
|
||||
OnPress: func() error {
|
||||
return f(true)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase bool, detached bool) error {
|
||||
opts := git_commands.NewWorktreeOpts{
|
||||
Base: base,
|
||||
Detach: detached,
|
||||
}
|
||||
|
||||
f := func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.AddWorktree)
|
||||
if err := self.c.Git().Worktree.New(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
return self.Switch(opts.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||
})
|
||||
}
|
||||
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.NewWorktreePath,
|
||||
HandleConfirm: func(path string) error {
|
||||
opts.Path = path
|
||||
|
||||
if detached {
|
||||
return f()
|
||||
}
|
||||
|
||||
if canCheckoutBase {
|
||||
title := utils.ResolvePlaceholderString(self.c.Tr.NewBranchNameLeaveBlank, map[string]string{"default": base})
|
||||
// prompt for the new branch name where a blank means we just check out the branch
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: title,
|
||||
HandleConfirm: func(branchName string) error {
|
||||
opts.Branch = branchName
|
||||
|
||||
return f()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// prompt for the new branch name where a blank means we just check out the branch
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.NewBranchName,
|
||||
HandleConfirm: func(branchName string) error {
|
||||
if branchName == "" {
|
||||
return self.c.ErrorMsg(self.c.Tr.BranchNameCannotBeBlank)
|
||||
}
|
||||
|
||||
opts.Branch = branchName
|
||||
|
||||
return f()
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) Switch(path string, contextKey types.ContextKey) error {
|
||||
if self.c.Git().Worktree.IsCurrentWorktree(path) {
|
||||
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.SwitchToWorktree)
|
||||
|
||||
return self.reposHelper.DispatchSwitchTo(path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
|
||||
title := self.c.Tr.RemoveWorktreeTitle
|
||||
var templateStr string
|
||||
if force {
|
||||
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
|
||||
} else {
|
||||
templateStr = self.c.Tr.RemoveWorktreePrompt
|
||||
}
|
||||
message := utils.ResolvePlaceholderString(
|
||||
templateStr,
|
||||
map[string]string{
|
||||
"worktreeName": worktree.Name(),
|
||||
},
|
||||
)
|
||||
|
||||
return self.c.Confirm(types.ConfirmOpts{
|
||||
Title: title,
|
||||
Prompt: message,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.RemoveWorktree)
|
||||
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !strings.Contains(errMessage, "--force") {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
if !force {
|
||||
return self.Remove(worktree, true)
|
||||
}
|
||||
return self.c.ErrorMsg(errMessage)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.RemovingWorktree)
|
||||
|
||||
err := self.c.Git().Worktree.Detach(worktree.Path)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) ViewWorktreeOptions(context types.IListContext, ref string) error {
|
||||
currentBranch := self.refsHelper.GetCheckedOutRef()
|
||||
canCheckoutBase := context == self.c.Contexts().Branches && ref != currentBranch.RefName()
|
||||
|
||||
return self.ViewBranchWorktreeOptions(ref, canCheckoutBase)
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string, canCheckoutBase bool) error {
|
||||
placeholders := map[string]string{"ref": branchName}
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.WorktreeTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
|
||||
OnPress: func() error {
|
||||
return self.NewWorktreeCheckout(branchName, canCheckoutBase, false)
|
||||
},
|
||||
},
|
||||
{
|
||||
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
|
||||
OnPress: func() error {
|
||||
return self.NewWorktreeCheckout(branchName, canCheckoutBase, true)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *WorktreeHelper) GetCurrentRepoName() string {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
// check if .git is a file or a directory
|
||||
gitPath := filepath.Join(pwd, ".git")
|
||||
gitFileInfo, err := os.Stat(gitPath)
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
|
||||
// must be a worktree or bare repo
|
||||
if !gitFileInfo.IsDir() {
|
||||
worktreeGitPath, ok := git_commands.WorktreeGitPath(pwd)
|
||||
if !ok {
|
||||
return basePath()
|
||||
}
|
||||
|
||||
// now we just jump up three directories to get the repo name
|
||||
return filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(worktreeGitPath))))
|
||||
}
|
||||
|
||||
return basePath()
|
||||
}
|
||||
|
||||
func basePath() string {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
return filepath.Base(pwd)
|
||||
}
|
||||
@@ -267,22 +267,22 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
|
||||
return self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
CommitIndex: self.context().GetSelectedLineIdx(),
|
||||
InitialMessage: commitMessage,
|
||||
Title: self.c.Tr.Actions.RewordCommit,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: self.handleReword,
|
||||
CommitIndex: self.context().GetSelectedLineIdx(),
|
||||
InitialMessage: commitMessage,
|
||||
SummaryTitle: self.c.Tr.Actions.RewordCommit,
|
||||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: self.handleReword,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleReword(message string) error {
|
||||
err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), message)
|
||||
func (self *LocalCommitsController) handleReword(summary string, description string) error {
|
||||
err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), summary, description)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.c.Helpers().Commits.OnCommitSuccess()
|
||||
_ = self.c.Helpers().Commits.PopCommitMessageContexts()
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
}
|
||||
|
||||
@@ -682,7 +682,7 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
||||
return self.c.Helpers().Tags.CreateTagMenu(commit.Sha, func() {})
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) openSearch() error {
|
||||
|
||||
@@ -53,7 +53,9 @@ func (self *MenuController) GetOnClick() func() error {
|
||||
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
|
||||
return func(types.OnFocusOpts) error {
|
||||
selectedMenuItem := self.context().GetSelected()
|
||||
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
|
||||
if selectedMenuItem != nil {
|
||||
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -37,9 +38,10 @@ func (self *OptionsMenuAction) Call() error {
|
||||
})
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.Keybindings,
|
||||
Items: menuItems,
|
||||
HideCancel: true,
|
||||
Title: self.c.Tr.Keybindings,
|
||||
Items: menuItems,
|
||||
HideCancel: true,
|
||||
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
@@ -78,7 +77,7 @@ func (self *QuitActions) Escape() error {
|
||||
|
||||
repoPathStack := self.c.State().GetRepoPathStack()
|
||||
if !repoPathStack.IsEmpty() {
|
||||
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true, context.NO_CONTEXT)
|
||||
return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true)
|
||||
}
|
||||
|
||||
if self.c.UserConfig.QuitOnTopLevelReturn {
|
||||
|
||||
@@ -70,6 +70,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
|
||||
}
|
||||
|
||||
self.setSubCommits(commits)
|
||||
self.c.Helpers().Refresh.RefreshAuthors(commits)
|
||||
|
||||
subCommitsContext := self.c.Contexts().SubCommits
|
||||
subCommitsContext.SetSelectedLineIdx(0)
|
||||
|
||||
@@ -141,7 +141,7 @@ func (self *TagsController) createResetMenu(tag *models.Tag) error {
|
||||
|
||||
func (self *TagsController) create() error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
return self.c.Helpers().Tags.CreateTagMenu("", func() { self.context().SetSelectedLineIdx(0) })
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt("", func() { self.context().SetSelectedLineIdx(0) })
|
||||
}
|
||||
|
||||
func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// This controller is for all contexts that have items you can create a worktree from
|
||||
|
||||
var _ types.IController = &WorktreeOptionsController{}
|
||||
|
||||
type CanViewWorktreeOptions interface {
|
||||
types.IListContext
|
||||
}
|
||||
|
||||
type WorktreeOptionsController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
context CanViewWorktreeOptions
|
||||
}
|
||||
|
||||
func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController {
|
||||
return &WorktreeOptionsController{
|
||||
baseController: baseController{},
|
||||
c: controllerCommon,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions),
|
||||
Handler: self.checkSelected(self.viewWorktreeOptions),
|
||||
Description: self.c.Tr.ViewWorktreeOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *WorktreeOptionsController) checkSelected(callback func(string) error) func() error {
|
||||
return func() error {
|
||||
ref := self.context.GetSelectedItemId()
|
||||
if ref == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(ref)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreeOptionsController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error {
|
||||
return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type WorktreesController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
}
|
||||
|
||||
var _ types.IController = &WorktreesController{}
|
||||
|
||||
func NewWorktreesController(
|
||||
common *ControllerCommon,
|
||||
) *WorktreesController {
|
||||
return &WorktreesController{
|
||||
baseController: baseController{},
|
||||
c: common,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
bindings := []*types.Binding{
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Select),
|
||||
Handler: self.checkSelected(self.enter),
|
||||
Description: self.c.Tr.SwitchToWorktree,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.Remove),
|
||||
Handler: self.checkSelected(self.remove),
|
||||
Description: self.c.Tr.RemoveWorktree,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.New),
|
||||
Handler: self.add,
|
||||
Description: self.c.Tr.CreateWorktree,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Universal.OpenFile),
|
||||
Handler: self.checkSelected(self.open),
|
||||
Description: self.c.Tr.OpenInEditor,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (self *WorktreesController) GetOnRenderToMain() func() error {
|
||||
return func() error {
|
||||
var task types.UpdateTask
|
||||
worktree := self.context().GetSelected()
|
||||
if worktree == nil {
|
||||
task = types.NewRenderStringTask(self.c.Tr.NoWorktreesThisRepo)
|
||||
} else {
|
||||
main := ""
|
||||
if worktree.Main() {
|
||||
main = style.FgDefault.Sprintf(" %s", self.c.Tr.MainWorktree)
|
||||
}
|
||||
|
||||
missing := ""
|
||||
if self.c.Git().Worktree.IsWorktreePathMissing(worktree.Path) {
|
||||
missing = style.FgRed.Sprintf(" %s", self.c.Tr.MissingWorktree)
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
w := tabwriter.NewWriter(&builder, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Name, style.FgGreen.Sprint(worktree.Name()), main)
|
||||
_, _ = fmt.Fprintf(w, "%s:\t%s\n", self.c.Tr.Branch, style.FgYellow.Sprint(worktree.Branch))
|
||||
_, _ = fmt.Fprintf(w, "%s:\t%s%s\n", self.c.Tr.Path, style.FgCyan.Sprint(worktree.Path), missing)
|
||||
_ = w.Flush()
|
||||
|
||||
task = types.NewRenderStringTask(builder.String())
|
||||
}
|
||||
|
||||
return self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
Pair: self.c.MainViewPairs().Normal,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
Title: self.c.Tr.WorktreeTitle,
|
||||
Task: task,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreesController) add() error {
|
||||
return self.c.Helpers().Worktree.NewWorktree()
|
||||
}
|
||||
|
||||
func (self *WorktreesController) remove(worktree *models.Worktree) error {
|
||||
if worktree.Main() {
|
||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteMainWorktree)
|
||||
}
|
||||
|
||||
if self.c.Git().Worktree.IsCurrentWorktree(worktree.Path) {
|
||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
|
||||
}
|
||||
|
||||
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||
}
|
||||
|
||||
func (self *WorktreesController) GetOnClick() func() error {
|
||||
return self.checkSelected(self.enter)
|
||||
}
|
||||
|
||||
func (self *WorktreesController) enter(worktree *models.Worktree) error {
|
||||
return self.c.Helpers().Worktree.Switch(worktree.Path, context.WORKTREES_CONTEXT_KEY)
|
||||
}
|
||||
|
||||
func (self *WorktreesController) open(worktree *models.Worktree) error {
|
||||
return self.c.Helpers().Files.OpenFile(worktree.Path)
|
||||
}
|
||||
|
||||
func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error {
|
||||
return func() error {
|
||||
worktree := self.context().GetSelected()
|
||||
if worktree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(worktree)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *WorktreesController) Context() types.Context {
|
||||
return self.context()
|
||||
}
|
||||
|
||||
func (self *WorktreesController) context() *context.WorktreesContext {
|
||||
return self.c.Contexts().Worktrees
|
||||
}
|
||||
+44
-41
@@ -132,6 +132,8 @@ type Gui struct {
|
||||
helpers *helpers.Helpers
|
||||
|
||||
integrationTest integrationTypes.IntegrationTest
|
||||
|
||||
afterLayoutFuncs chan func() error
|
||||
}
|
||||
|
||||
type StateAccessor struct {
|
||||
@@ -274,7 +276,7 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
|
||||
return self.SplitMainPanel
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool, contextKey types.ContextKey) error {
|
||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
||||
var err error
|
||||
gui.git, err = commands.NewGitCommand(
|
||||
gui.Common,
|
||||
@@ -295,17 +297,6 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool, context
|
||||
return err
|
||||
}
|
||||
|
||||
// if a context key has been given, push that instead, and set its index to 0
|
||||
if contextKey != context.NO_CONTEXT {
|
||||
contextToPush = gui.c.ContextForKey(contextKey)
|
||||
// when we pass a list context, the expectation is that our cursor goes to the top,
|
||||
// because e.g. with worktrees, we'll show the current worktree at the top of the list.
|
||||
listContext, ok := contextToPush.(types.IListContext)
|
||||
if ok {
|
||||
listContext.GetList().SetSelectedLineIdx(0)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.c.PushContext(contextToPush); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -358,6 +349,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
BisectInfo: git_commands.NewNullBisectInfo(),
|
||||
FilesTrie: patricia.NewTrie(),
|
||||
Authors: map[string]*models.Author{},
|
||||
},
|
||||
Modes: &types.Modes{
|
||||
Filtering: filtering.New(startArgs.FilterPath),
|
||||
@@ -465,11 +457,13 @@ func NewGui(
|
||||
SyncMutex: &deadlock.Mutex{},
|
||||
LocalCommitsMutex: &deadlock.Mutex{},
|
||||
SubCommitsMutex: &deadlock.Mutex{},
|
||||
AuthorsMutex: &deadlock.Mutex{},
|
||||
SubprocessMutex: &deadlock.Mutex{},
|
||||
PopupMutex: &deadlock.Mutex{},
|
||||
PtyMutex: &deadlock.Mutex{},
|
||||
},
|
||||
InitialDir: initialDir,
|
||||
InitialDir: initialDir,
|
||||
afterLayoutFuncs: make(chan func() error, 1000),
|
||||
}
|
||||
|
||||
gui.WatchFilesForChanges()
|
||||
@@ -530,9 +524,29 @@ var RuneReplacements = map[rune]string{
|
||||
}
|
||||
|
||||
func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) {
|
||||
playRecording := test != nil && os.Getenv(components.SANDBOX_ENV_VAR) != "true"
|
||||
runInSandbox := os.Getenv(components.SANDBOX_ENV_VAR) == "true"
|
||||
playRecording := test != nil && !runInSandbox
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playRecording, headless, RuneReplacements)
|
||||
width, height := 0, 0
|
||||
if test != nil {
|
||||
if test.RequiresHeadless() {
|
||||
if runInSandbox {
|
||||
panic("Test requires headless, can't run in sandbox")
|
||||
}
|
||||
headless = true
|
||||
}
|
||||
width, height = test.HeadlessDimensions()
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||
OutputMode: gocui.OutputTrue,
|
||||
SupportOverlaps: OverlappingEdges,
|
||||
PlayRecording: playRecording,
|
||||
Headless: headless,
|
||||
RuneReplacements: RuneReplacements,
|
||||
Width: width,
|
||||
Height: height,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -541,32 +555,21 @@ func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest)
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
||||
branchesTabs := []context.TabView{
|
||||
{
|
||||
Tab: gui.c.Tr.LocalBranchesTitle,
|
||||
ViewName: "localBranches",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.RemotesTitle,
|
||||
ViewName: "remotes",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.TagsTitle,
|
||||
ViewName: "tags",
|
||||
},
|
||||
}
|
||||
|
||||
if gui.c.Git().Version.SupportsWorktrees() {
|
||||
branchesTabs = append(branchesTabs,
|
||||
context.TabView{
|
||||
Tab: gui.c.Tr.WorktreesTitle,
|
||||
ViewName: "worktrees",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return map[string][]context.TabView{
|
||||
"branches": branchesTabs,
|
||||
"branches": {
|
||||
{
|
||||
Tab: gui.c.Tr.LocalBranchesTitle,
|
||||
ViewName: "localBranches",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.RemotesTitle,
|
||||
ViewName: "remotes",
|
||||
},
|
||||
{
|
||||
Tab: gui.c.Tr.TagsTitle,
|
||||
ViewName: "tags",
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
Tab: gui.c.Tr.CommitsTitle,
|
||||
@@ -636,7 +639,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
}
|
||||
|
||||
// onNewRepo must be called after g.SetManager because SetManager deletes keybindings
|
||||
if err := gui.onNewRepo(startArgs, false, context.NO_CONTEXT); err != nil {
|
||||
if err := gui.onNewRepo(startArgs, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -76,10 +76,6 @@ func (self *guiCommon) Context() types.IContextMgr {
|
||||
return self.gui.State.ContextMgr
|
||||
}
|
||||
|
||||
func (self *guiCommon) ContextForKey(key types.ContextKey) types.Context {
|
||||
return self.gui.State.ContextMgr.ContextForKey(key)
|
||||
}
|
||||
|
||||
func (self *guiCommon) ActivateContext(context types.Context) error {
|
||||
return self.gui.State.ContextMgr.ActivateContext(context, types.OnFocusOpts{})
|
||||
}
|
||||
@@ -172,3 +168,12 @@ func (self *guiCommon) IsAnyModeActive() bool {
|
||||
func (self *guiCommon) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []*gocui.ViewMouseBinding) {
|
||||
return self.gui.GetInitialKeybindingsWithCustomCommands()
|
||||
}
|
||||
|
||||
func (self *guiCommon) AfterLayout(f func() error) {
|
||||
select {
|
||||
case self.gui.afterLayoutFuncs <- f:
|
||||
default:
|
||||
// hopefully this never happens
|
||||
self.gui.c.Log.Error("afterLayoutFuncs channel is full, skipping function")
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -149,7 +149,23 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
// if you run `lazygit --logs`
|
||||
// this will let you see these branches as prettified json
|
||||
// gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4]))
|
||||
return gui.helpers.Confirmation.ResizeCurrentPopupPanel()
|
||||
if err := gui.helpers.Confirmation.ResizeCurrentPopupPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case f := <-gui.afterLayoutFuncs:
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Contexts.Menu.SetMenuItems(opts.Items)
|
||||
gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment)
|
||||
gui.State.Contexts.Menu.SetSelectedLineIdx(0)
|
||||
|
||||
gui.Views.Menu.Title = opts.Title
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var branchPrefixColorCache = make(map[string]style.TextStyle)
|
||||
@@ -24,11 +22,10 @@ func GetBranchListDisplayStrings(
|
||||
diffName string,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
) [][]string {
|
||||
return slices.Map(branches, func(branch *models.Branch) []string {
|
||||
diffed := branch.Name == diffName
|
||||
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, worktrees)
|
||||
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +36,6 @@ func getBranchDisplayStrings(
|
||||
diffed bool,
|
||||
tr *i18n.TranslationSet,
|
||||
userConfig *config.UserConfig,
|
||||
worktrees []*models.Worktree,
|
||||
) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
@@ -53,10 +49,6 @@ func getBranchDisplayStrings(
|
||||
|
||||
coloredName := nameTextStyle.Sprint(displayName)
|
||||
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft)
|
||||
if git_commands.CheckedOutByOtherWorktree(b, worktrees) {
|
||||
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree))
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
|
||||
}
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
|
||||
|
||||
recencyColor := style.FgCyan
|
||||
@@ -66,7 +58,6 @@ func getBranchDisplayStrings(
|
||||
|
||||
res := make([]string, 0, 6)
|
||||
res = append(res, recencyColor.Sprint(b.Recency))
|
||||
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, nameTextStyle.Sprint(icons.IconForBranch(b)))
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
BRANCH_ICON = "\U000f062c" //
|
||||
DETACHED_HEAD_ICON = "\ue729" //
|
||||
TAG_ICON = "\uf02b" //
|
||||
COMMIT_ICON = "\U000f0718" //
|
||||
MERGE_COMMIT_ICON = "\U000f062d" //
|
||||
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
||||
STASH_ICON = "\uf01c" //
|
||||
LINKED_WORKTREE_ICON = "\uf838" //
|
||||
MISSING_LINKED_WORKTREE_ICON = "\uf839" //
|
||||
BRANCH_ICON = "\U000f062c" //
|
||||
DETACHED_HEAD_ICON = "\ue729" //
|
||||
TAG_ICON = "\uf02b" //
|
||||
COMMIT_ICON = "\U000f0718" //
|
||||
MERGE_COMMIT_ICON = "\U000f062d" //
|
||||
DEFAULT_REMOTE_ICON = "\uf02a2" //
|
||||
STASH_ICON = "\uf01c" //
|
||||
)
|
||||
|
||||
var remoteIcons = map[string]string{
|
||||
@@ -70,10 +68,3 @@ func IconForRemote(remote *models.Remote) string {
|
||||
func IconForStash(stash *models.StashEntry) string {
|
||||
return STASH_ICON
|
||||
}
|
||||
|
||||
func IconForWorktree(missing bool) string {
|
||||
if missing {
|
||||
return MISSING_LINKED_WORKTREE_ICON
|
||||
}
|
||||
return LINKED_WORKTREE_ICON
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
)
|
||||
|
||||
func FormatStatus(repoName string, currentBranch *models.Branch, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string {
|
||||
status := ""
|
||||
|
||||
if currentBranch.IsRealBranch() {
|
||||
status += ColoredBranchStatus(currentBranch, tr) + " "
|
||||
}
|
||||
|
||||
if workingTreeState != enums.REBASE_MODE_NONE {
|
||||
status += style.FgYellow.Sprintf("(%s) ", FormatWorkingTreeStateLower(tr, workingTreeState))
|
||||
}
|
||||
|
||||
name := GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
|
||||
// If the user is in a linked worktree (i.e. not the main worktree) we'll display that
|
||||
if linkedWorktreeName != "" {
|
||||
icon := ""
|
||||
if icons.IsIconEnabled() {
|
||||
icon = icons.LINKED_WORKTREE_ICON + " "
|
||||
}
|
||||
repoName = fmt.Sprintf("%s(%s%s)", repoName, icon, style.FgCyan.Sprint(linkedWorktreeName))
|
||||
}
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
return status
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetWorktreeDisplayStrings(worktrees []*models.Worktree, isCurrent func(string) bool, isMissing func(string) bool) [][]string {
|
||||
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
|
||||
return GetWorktreeDisplayString(
|
||||
isCurrent(worktree.Path),
|
||||
isMissing(worktree.Path),
|
||||
worktree)
|
||||
})
|
||||
}
|
||||
|
||||
func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *models.Worktree) []string {
|
||||
textStyle := theme.DefaultTextColor
|
||||
|
||||
current := ""
|
||||
currentColor := style.FgCyan
|
||||
if isCurrent {
|
||||
current = " *"
|
||||
currentColor = style.FgGreen
|
||||
}
|
||||
|
||||
icon := icons.IconForWorktree(false)
|
||||
if isPathMissing {
|
||||
textStyle = style.FgRed
|
||||
icon = icons.IconForWorktree(true)
|
||||
}
|
||||
|
||||
res := []string{}
|
||||
res = append(res, currentColor.Sprint(current))
|
||||
if icons.IsIconEnabled() {
|
||||
res = append(res, textStyle.Sprint(icon))
|
||||
}
|
||||
|
||||
name := worktree.Name()
|
||||
if worktree.Main() {
|
||||
// TODO: i18n
|
||||
name += " (main worktree)"
|
||||
}
|
||||
res = append(res, textStyle.Sprint(name))
|
||||
return res
|
||||
}
|
||||
+11
-5
@@ -66,7 +66,6 @@ type IGuiCommon interface {
|
||||
IsCurrentContext(Context) bool
|
||||
// TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push()
|
||||
Context() IContextMgr
|
||||
ContextForKey(key ContextKey) Context
|
||||
|
||||
ActivateContext(context Context) error
|
||||
|
||||
@@ -81,6 +80,10 @@ type IGuiCommon interface {
|
||||
// Runs a function in a goroutine. Use this whenever you want to run a goroutine and keep track of the fact
|
||||
// that lazygit is still busy. See docs/dev/Busy.md
|
||||
OnWorker(f func(gocui.Task))
|
||||
// Function to call at the end of our 'layout' function which renders views
|
||||
// For example, you may want a view's line to be focused only after that view is
|
||||
// resized, if in accordion mode.
|
||||
AfterLayout(f func() error)
|
||||
|
||||
// returns the gocui Gui struct. There is a good chance you don't actually want to use
|
||||
// this struct and instead want to use another method above
|
||||
@@ -130,9 +133,10 @@ type IPopupHandler interface {
|
||||
}
|
||||
|
||||
type CreateMenuOptions struct {
|
||||
Title string
|
||||
Items []*MenuItem
|
||||
HideCancel bool
|
||||
Title string
|
||||
Items []*MenuItem
|
||||
HideCancel bool
|
||||
ColumnAlignment []utils.Alignment
|
||||
}
|
||||
|
||||
type CreatePopupPanelOpts struct {
|
||||
@@ -197,7 +201,6 @@ type Model struct {
|
||||
StashEntries []*models.StashEntry
|
||||
SubCommits []*models.Commit
|
||||
Remotes []*models.Remote
|
||||
Worktrees []*models.Worktree
|
||||
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
// when in filtering mode we only include the ones that match the given path
|
||||
@@ -214,6 +217,8 @@ type Model struct {
|
||||
|
||||
// for displaying suggestions while typing in a file name
|
||||
FilesTrie *patricia.Trie
|
||||
|
||||
Authors map[string]*models.Author
|
||||
}
|
||||
|
||||
// if you add a new mutex here be sure to instantiate it. We're using pointers to
|
||||
@@ -225,6 +230,7 @@ type Mutexes struct {
|
||||
SyncMutex *deadlock.Mutex
|
||||
LocalCommitsMutex *deadlock.Mutex
|
||||
SubCommitsMutex *deadlock.Mutex
|
||||
AuthorsMutex *deadlock.Mutex
|
||||
SubprocessMutex *deadlock.Mutex
|
||||
PopupMutex *deadlock.Mutex
|
||||
PtyMutex *deadlock.Mutex
|
||||
|
||||
@@ -13,7 +13,6 @@ const (
|
||||
REFLOG
|
||||
TAGS
|
||||
REMOTES
|
||||
WORKTREES
|
||||
STATUS
|
||||
SUBMODULES
|
||||
STAGING
|
||||
|
||||
@@ -8,7 +8,6 @@ type Views struct {
|
||||
Files *gocui.View
|
||||
Branches *gocui.View
|
||||
Remotes *gocui.View
|
||||
Worktrees *gocui.View
|
||||
Tags *gocui.View
|
||||
RemoteBranches *gocui.View
|
||||
ReflogCommits *gocui.View
|
||||
|
||||
@@ -29,7 +29,6 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
{viewPtr: &gui.Views.Files, name: "files"},
|
||||
{viewPtr: &gui.Views.Tags, name: "tags"},
|
||||
{viewPtr: &gui.Views.Remotes, name: "remotes"},
|
||||
{viewPtr: &gui.Views.Worktrees, name: "worktrees"},
|
||||
{viewPtr: &gui.Views.Branches, name: "localBranches"},
|
||||
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
|
||||
{viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"},
|
||||
@@ -114,8 +113,6 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle
|
||||
|
||||
gui.Views.Worktrees.Title = gui.c.Tr.WorktreesTitle
|
||||
|
||||
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
|
||||
|
||||
gui.Views.Files.Title = gui.c.Tr.FilesTitle
|
||||
|
||||
+3
-4
@@ -181,7 +181,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
RecentRepos: "最近的仓库",
|
||||
MergeOptionsTitle: "合并选项",
|
||||
RebaseOptionsTitle: "变基选项",
|
||||
CommitMessageTitle: "提交讯息",
|
||||
CommitSummaryTitle: "提交讯息",
|
||||
LocalBranchesTitle: "分支页面",
|
||||
SearchTitle: "搜索",
|
||||
TagsTitle: "标签页面",
|
||||
@@ -305,8 +305,8 @@ func chineseTranslationSet() TranslationSet {
|
||||
EditRemote: "编辑远程仓库",
|
||||
TagCommit: "标签提交",
|
||||
TagMenuTitle: "创建标签",
|
||||
TagNameTitle: "标签名称:",
|
||||
TagMessageTitle: "标签消息:",
|
||||
TagNameTitle: "标签名称",
|
||||
TagMessageTitle: "标签消息",
|
||||
AnnotatedTag: "附注标签",
|
||||
LightweightTag: "轻量标签",
|
||||
DeleteTag: "删除标签",
|
||||
@@ -315,7 +315,6 @@ func chineseTranslationSet() TranslationSet {
|
||||
PushTagTitle: "将 {{.tagName}} 推送到远程仓库:",
|
||||
PushTag: "推送标签",
|
||||
CreateTag: "创建标签",
|
||||
CreateTagTitle: "标签名称:",
|
||||
FetchRemote: "抓取远程仓库",
|
||||
FetchingRemoteStatus: "抓取远程仓库中",
|
||||
CheckoutCommit: "检出提交",
|
||||
|
||||
+2
-3
@@ -146,7 +146,7 @@ func dutchTranslationSet() TranslationSet {
|
||||
RecentRepos: "Recente repositories",
|
||||
MergeOptionsTitle: "Merge opties",
|
||||
RebaseOptionsTitle: "Rebase opties",
|
||||
CommitMessageTitle: "Commit bericht",
|
||||
CommitSummaryTitle: "Commit bericht",
|
||||
LocalBranchesTitle: "Branches",
|
||||
SearchTitle: "Zoek",
|
||||
TagsTitle: "Tags",
|
||||
@@ -263,14 +263,13 @@ func dutchTranslationSet() TranslationSet {
|
||||
SetUpstreamMessage: "Weet je zeker dat je de upstream branch van '{{.checkedOut}}' naar '{{.selected}}' wilt zetten",
|
||||
EditRemote: "Wijzig remote",
|
||||
TagCommit: "Tag commit",
|
||||
TagNameTitle: "Tag naam:",
|
||||
TagNameTitle: "Tag naam",
|
||||
DeleteTag: "Verwijder tag",
|
||||
DeleteTagTitle: "Verwijder tag",
|
||||
DeleteTagPrompt: "Weet je zeker dat je '{{.tagName}}' wil verwijderen?",
|
||||
PushTagTitle: "Remote om tag '{{.tagName}}' te pushen naar:",
|
||||
PushTag: "Push tag",
|
||||
CreateTag: "Creëer tag",
|
||||
CreateTagTitle: "Tag naam:",
|
||||
FetchRemote: "Fetch remote",
|
||||
FetchingRemoteStatus: "Remote fetchen",
|
||||
CheckoutCommit: "Checkout commit",
|
||||
|
||||
+6
-74
@@ -156,7 +156,6 @@ type TranslationSet struct {
|
||||
GitconfigParseErr string
|
||||
EditFile string
|
||||
OpenFile string
|
||||
OpenInEditor string
|
||||
IgnoreFile string
|
||||
ExcludeFile string
|
||||
RefreshFiles string
|
||||
@@ -193,7 +192,7 @@ type TranslationSet struct {
|
||||
RecentRepos string
|
||||
MergeOptionsTitle string
|
||||
RebaseOptionsTitle string
|
||||
CommitMessageTitle string
|
||||
CommitSummaryTitle string
|
||||
CommitDescriptionTitle string
|
||||
CommitDescriptionSubTitle string
|
||||
LocalBranchesTitle string
|
||||
@@ -356,7 +355,7 @@ type TranslationSet struct {
|
||||
PushTagTitle string
|
||||
PushTag string
|
||||
CreateTag string
|
||||
CreateTagTitle string
|
||||
CreatingTag string
|
||||
FetchRemote string
|
||||
FetchingRemoteStatus string
|
||||
CheckoutCommit string
|
||||
@@ -481,7 +480,6 @@ type TranslationSet struct {
|
||||
ErrCannotEditDirectory string
|
||||
ErrStageDirWithInlineMergeConflicts string
|
||||
ErrRepositoryMovedOrDeleted string
|
||||
ErrWorktreeMovedOrRemoved string
|
||||
CommandLog string
|
||||
ToggleShowCommandLog string
|
||||
FocusCommandLog string
|
||||
@@ -542,36 +540,6 @@ type TranslationSet struct {
|
||||
FilterPrefix string
|
||||
ExitSearchMode string
|
||||
ExitTextFilterMode string
|
||||
SwitchToWorktree string
|
||||
RemoveWorktree string
|
||||
RemoveWorktreeTitle string
|
||||
DetachWorktree string
|
||||
DetachingWorktree string
|
||||
WorktreesTitle string
|
||||
WorktreeTitle string
|
||||
RemoveWorktreePrompt string
|
||||
ForceRemoveWorktreePrompt string
|
||||
RemovingWorktree string
|
||||
AddingWorktree string
|
||||
CantDeleteCurrentWorktree string
|
||||
AlreadyInWorktree string
|
||||
CantDeleteMainWorktree string
|
||||
NoWorktreesThisRepo string
|
||||
MissingWorktree string
|
||||
MainWorktree string
|
||||
CreateWorktree string
|
||||
NewWorktreePath string
|
||||
NewWorktreeBase string
|
||||
BranchNameCannotBeBlank string
|
||||
NewBranchName string
|
||||
NewBranchNameLeaveBlank string
|
||||
ViewWorktreeOptions string
|
||||
CreateWorktreeFrom string
|
||||
CreateWorktreeFromDetached string
|
||||
LcWorktree string
|
||||
Name string
|
||||
Branch string
|
||||
Path string
|
||||
Actions Actions
|
||||
Bisect Bisect
|
||||
}
|
||||
@@ -698,8 +666,6 @@ type Actions struct {
|
||||
ResetBisect string
|
||||
BisectSkip string
|
||||
BisectMark string
|
||||
RemoveWorktree string
|
||||
AddWorktree string
|
||||
}
|
||||
|
||||
const englishIntroPopupMessage = `
|
||||
@@ -883,7 +849,6 @@ func EnglishTranslationSet() TranslationSet {
|
||||
GitconfigParseErr: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
|
||||
EditFile: `Edit file`,
|
||||
OpenFile: `Open file`,
|
||||
OpenInEditor: "Open in editor",
|
||||
IgnoreFile: `Add to .gitignore`,
|
||||
ExcludeFile: `Add to .git/info/exclude`,
|
||||
RefreshFiles: `Refresh files`,
|
||||
@@ -919,7 +884,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
RecentRepos: "Recent repositories",
|
||||
MergeOptionsTitle: "Merge options",
|
||||
RebaseOptionsTitle: "Rebase options",
|
||||
CommitMessageTitle: "Commit summary",
|
||||
CommitSummaryTitle: "Commit summary",
|
||||
CommitDescriptionTitle: "Commit description",
|
||||
CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus",
|
||||
LocalBranchesTitle: "Local branches",
|
||||
@@ -1074,8 +1039,8 @@ func EnglishTranslationSet() TranslationSet {
|
||||
EditRemote: "Edit remote",
|
||||
TagCommit: "Tag commit",
|
||||
TagMenuTitle: "Create tag",
|
||||
TagNameTitle: "Tag name:",
|
||||
TagMessageTitle: "Tag message:",
|
||||
TagNameTitle: "Tag name",
|
||||
TagMessageTitle: "Tag description",
|
||||
AnnotatedTag: "Annotated tag",
|
||||
LightweightTag: "Lightweight tag",
|
||||
DeleteTag: "Delete tag",
|
||||
@@ -1084,7 +1049,7 @@ func EnglishTranslationSet() TranslationSet {
|
||||
PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
|
||||
PushTag: "Push tag",
|
||||
CreateTag: "Create tag",
|
||||
CreateTagTitle: "Tag name:",
|
||||
CreatingTag: "Creating tag",
|
||||
FetchRemote: "Fetch remote",
|
||||
FetchingRemoteStatus: "Fetching remote",
|
||||
CheckoutCommit: "Checkout commit",
|
||||
@@ -1210,7 +1175,6 @@ func EnglishTranslationSet() TranslationSet {
|
||||
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
|
||||
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
||||
CommandLog: "Command log",
|
||||
ErrWorktreeMovedOrRemoved: "Cannot find worktree. It might have been moved or removed ¯\\_(ツ)_/¯",
|
||||
ToggleShowCommandLog: "Toggle show/hide command log",
|
||||
FocusCommandLog: "Focus command log",
|
||||
CommandLogHeader: "You can hide/focus this panel by pressing '%s'\n",
|
||||
@@ -1269,36 +1233,6 @@ func EnglishTranslationSet() TranslationSet {
|
||||
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
|
||||
SearchPrefix: "Search: ",
|
||||
FilterPrefix: "Filter: ",
|
||||
WorktreesTitle: "Worktrees",
|
||||
WorktreeTitle: "Worktree",
|
||||
SwitchToWorktree: "Switch to worktree",
|
||||
RemoveWorktree: "Remove worktree",
|
||||
RemoveWorktreeTitle: "Remove worktree",
|
||||
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
|
||||
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
|
||||
RemovingWorktree: "Deleting worktree",
|
||||
DetachWorktree: "Detach worktree",
|
||||
DetachingWorktree: "Detaching worktree",
|
||||
AddingWorktree: "Adding worktree",
|
||||
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
|
||||
AlreadyInWorktree: "You are already in the selected worktree",
|
||||
CantDeleteMainWorktree: "You cannot remove the main worktree!",
|
||||
NoWorktreesThisRepo: "No worktrees",
|
||||
MissingWorktree: "(missing)",
|
||||
MainWorktree: "(main)",
|
||||
CreateWorktree: "Create worktree",
|
||||
NewWorktreePath: "New worktree path",
|
||||
NewWorktreeBase: "New worktree base ref",
|
||||
BranchNameCannotBeBlank: "Branch name cannot be blank",
|
||||
NewBranchName: "New branch name",
|
||||
NewBranchNameLeaveBlank: "New branch name (leave blank to checkout {{.default}})",
|
||||
ViewWorktreeOptions: "View worktree options",
|
||||
CreateWorktreeFrom: "Create worktree from {{.ref}}",
|
||||
CreateWorktreeFromDetached: "Create worktree from {{.ref}} (detached)",
|
||||
LcWorktree: "worktree",
|
||||
Name: "Name",
|
||||
Branch: "Branch",
|
||||
Path: "Path",
|
||||
Actions: Actions{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
@@ -1406,8 +1340,6 @@ func EnglishTranslationSet() TranslationSet {
|
||||
ResetBisect: "Reset bisect",
|
||||
BisectSkip: "Bisect skip",
|
||||
BisectMark: "Bisect mark",
|
||||
RemoveWorktree: "Remove worktree",
|
||||
AddWorktree: "Add worktree",
|
||||
},
|
||||
Bisect: Bisect{
|
||||
Mark: "Mark %s as %s",
|
||||
|
||||
@@ -184,7 +184,7 @@ func japaneseTranslationSet() TranslationSet {
|
||||
RecentRepos: "最近使用したリポジトリ",
|
||||
// MergeOptionsTitle: "Merge Options",
|
||||
// RebaseOptionsTitle: "Rebase Options",
|
||||
CommitMessageTitle: "コミットメッセージ",
|
||||
CommitSummaryTitle: "コミットメッセージ",
|
||||
LocalBranchesTitle: "ブランチ",
|
||||
SearchTitle: "検索",
|
||||
TagsTitle: "タグ",
|
||||
@@ -315,8 +315,8 @@ func japaneseTranslationSet() TranslationSet {
|
||||
EditRemote: "リモートを編集",
|
||||
TagCommit: "タグを作成",
|
||||
TagMenuTitle: "タグを作成",
|
||||
TagNameTitle: "タグ名:",
|
||||
TagMessageTitle: "タグメッセージ: ",
|
||||
TagNameTitle: "タグ名",
|
||||
TagMessageTitle: "タグメッセージ",
|
||||
AnnotatedTag: "注釈付きタグ",
|
||||
LightweightTag: "軽量タグ",
|
||||
DeleteTag: "タグを削除",
|
||||
@@ -325,7 +325,6 @@ func japaneseTranslationSet() TranslationSet {
|
||||
PushTagTitle: "リモートにタグ '{{.tagName}}' をpush",
|
||||
PushTag: "タグをpush",
|
||||
CreateTag: "タグを作成",
|
||||
CreateTagTitle: "タグ名:",
|
||||
FetchRemote: "リモートをfetch",
|
||||
FetchingRemoteStatus: "リモートをfetch",
|
||||
CheckoutCommit: "コミットをチェックアウト",
|
||||
|
||||
+3
-4
@@ -182,7 +182,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
RecentRepos: "최근에 사용한 저장소",
|
||||
MergeOptionsTitle: "Merge options",
|
||||
RebaseOptionsTitle: "Rebase options",
|
||||
CommitMessageTitle: "커밋메시지",
|
||||
CommitSummaryTitle: "커밋메시지",
|
||||
LocalBranchesTitle: "브랜치",
|
||||
SearchTitle: "검색",
|
||||
TagsTitle: "태그",
|
||||
@@ -310,8 +310,8 @@ func koreanTranslationSet() TranslationSet {
|
||||
EditRemote: "Remote를 수정",
|
||||
TagCommit: "Tag commit",
|
||||
TagMenuTitle: "태그 작성",
|
||||
TagNameTitle: "태그 이름:",
|
||||
TagMessageTitle: "태그 메시지: ",
|
||||
TagNameTitle: "태그 이름",
|
||||
TagMessageTitle: "태그 메시지",
|
||||
AnnotatedTag: "Annotated tag",
|
||||
LightweightTag: "Lightweight tag",
|
||||
DeleteTag: "태그 삭제",
|
||||
@@ -320,7 +320,6 @@ func koreanTranslationSet() TranslationSet {
|
||||
PushTagTitle: "원격에 태그 '{{.tagName}}' 를 푸시",
|
||||
PushTag: "태그를 push",
|
||||
CreateTag: "태그를 생성",
|
||||
CreateTagTitle: "태그 이름:",
|
||||
FetchRemote: "원격을 업데이트",
|
||||
FetchingRemoteStatus: "원격을 업데이트 중",
|
||||
CheckoutCommit: "커밋을 체크아웃",
|
||||
|
||||
+3
-4
@@ -216,7 +216,7 @@ func RussianTranslationSet() TranslationSet {
|
||||
RecentRepos: "Последние репозитории",
|
||||
MergeOptionsTitle: "Параметры слияния",
|
||||
RebaseOptionsTitle: "Параметры перебазирования",
|
||||
CommitMessageTitle: "Сводка коммита",
|
||||
CommitSummaryTitle: "Сводка коммита",
|
||||
CommitDescriptionTitle: "Описание коммита",
|
||||
CommitDescriptionSubTitle: "Нажмите вкладку, чтобы переключить фокус",
|
||||
LocalBranchesTitle: "Локальные Ветки",
|
||||
@@ -371,8 +371,8 @@ func RussianTranslationSet() TranslationSet {
|
||||
EditRemote: "Редактировать удалённый репозитории",
|
||||
TagCommit: "Пометить коммит тегом",
|
||||
TagMenuTitle: "Создать тег",
|
||||
TagNameTitle: "Название тега:",
|
||||
TagMessageTitle: "Сообщения тега:",
|
||||
TagNameTitle: "Название тега",
|
||||
TagMessageTitle: "Сообщения тега",
|
||||
AnnotatedTag: "Аннотированный тег",
|
||||
LightweightTag: "Легковесный тег",
|
||||
DeleteTag: "Удалить тег",
|
||||
@@ -381,7 +381,6 @@ func RussianTranslationSet() TranslationSet {
|
||||
PushTagTitle: "Удалённый репозитории для отправки тега '{{.tagName}}' в:",
|
||||
PushTag: "Отправить тег",
|
||||
CreateTag: "Создать тег",
|
||||
CreateTagTitle: "Название тега:",
|
||||
FetchRemote: "Получение изменения из удалённого репозитория",
|
||||
FetchingRemoteStatus: "Получение статуса удалённого репозитория",
|
||||
CheckoutCommit: "Переключить коммит",
|
||||
|
||||
@@ -247,7 +247,7 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
RecentRepos: "最近的版本庫",
|
||||
MergeOptionsTitle: "合併選項",
|
||||
RebaseOptionsTitle: "變基選項",
|
||||
CommitMessageTitle: "提交摘要",
|
||||
CommitSummaryTitle: "提交摘要",
|
||||
CommitDescriptionTitle: "提交描述",
|
||||
CommitDescriptionSubTitle: "按 tab 切換焦點",
|
||||
LocalBranchesTitle: "本地分支",
|
||||
@@ -398,8 +398,8 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
EditRemote: "編輯遠端",
|
||||
TagCommit: "打標籤到提交",
|
||||
TagMenuTitle: "建立標籤",
|
||||
TagNameTitle: "標籤名稱:",
|
||||
TagMessageTitle: "標籤訊息:",
|
||||
TagNameTitle: "標籤名稱",
|
||||
TagMessageTitle: "標籤訊息",
|
||||
AnnotatedTag: "附註標籤",
|
||||
LightweightTag: "輕量標籤",
|
||||
DeleteTag: "刪除標籤",
|
||||
@@ -408,7 +408,6 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
PushTagTitle: "推送標籤 '{{.tagName}}' 至遠端:",
|
||||
PushTag: "推送標籤",
|
||||
CreateTag: "建立標籤",
|
||||
CreateTagTitle: "標籤名稱:",
|
||||
FetchRemote: "擷取遠端",
|
||||
FetchingRemoteStatus: "正在擷取遠端",
|
||||
CheckoutCommit: "檢出提交",
|
||||
|
||||
@@ -28,7 +28,10 @@ func RunTUI() {
|
||||
app := newApp(testDir)
|
||||
app.loadTests()
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, false, false, false, gui.RuneReplacements)
|
||||
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||
OutputMode: gocui.OutputTrue,
|
||||
RuneReplacements: gui.RuneReplacements,
|
||||
})
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
@@ -23,3 +23,9 @@ func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDr
|
||||
self.t.press(self.t.keys.Universal.Confirm)
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
|
||||
self.getViewDriver().Title(expected)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import (
|
||||
// to get the test's name via it's file's path.
|
||||
const unitTestDescription = "test test"
|
||||
|
||||
const (
|
||||
defaultWidth = 100
|
||||
defaultHeight = 100
|
||||
)
|
||||
|
||||
type IntegrationTest struct {
|
||||
name string
|
||||
description string
|
||||
@@ -32,6 +37,8 @@ type IntegrationTest struct {
|
||||
keys config.KeybindingConfig,
|
||||
)
|
||||
gitVersion GitVersionRestriction
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
|
||||
@@ -52,6 +59,11 @@ type NewIntegrationTestArgs struct {
|
||||
Skip bool
|
||||
// to run a test only on certain git versions
|
||||
GitVersion GitVersionRestriction
|
||||
// width and height when running in headless mode, for testing
|
||||
// the UI in different sizes.
|
||||
// If these are set, the test must be run in headless mode
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type GitVersionRestriction struct {
|
||||
@@ -120,6 +132,8 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
setupConfig: args.SetupConfig,
|
||||
run: args.Run,
|
||||
gitVersion: args.GitVersion,
|
||||
width: args.Width,
|
||||
height: args.Height,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +186,18 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) HeadlessDimensions() (int, int) {
|
||||
if self.width == 0 && self.height == 0 {
|
||||
return defaultWidth, defaultHeight
|
||||
}
|
||||
|
||||
return self.width, self.height
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) RequiresHeadless() bool {
|
||||
return self.width != 0 && self.height != 0
|
||||
}
|
||||
|
||||
func testNameFromCurrentFilePath() string {
|
||||
path := utils.FilePath(3)
|
||||
return TestNameFromFilePath(path)
|
||||
|
||||
@@ -82,6 +82,20 @@ func (self *ViewDriver) TopLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
return self.assertLines(0, matchers...)
|
||||
}
|
||||
|
||||
// Asserts on the visible lines of the view.
|
||||
// Note, this assumes that the view's viewport is filled with lines
|
||||
func (self *ViewDriver) VisibleLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
self.validateMatchersPassed(matchers)
|
||||
self.validateVisibleLineCount(matchers)
|
||||
|
||||
// Get the origin of the view and offset that.
|
||||
// Note that we don't do any retrying here so if we want to bring back retry logic
|
||||
// we'll need to update this.
|
||||
originY := self.getView().OriginY()
|
||||
|
||||
return self.assertLines(originY, matchers...)
|
||||
}
|
||||
|
||||
// asserts that somewhere in the view there are consequetive lines matching the given matchers.
|
||||
func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
self.validateMatchersPassed(matchers)
|
||||
@@ -212,6 +226,16 @@ func (self *ViewDriver) validateEnoughLines(matchers []*TextMatcher) {
|
||||
})
|
||||
}
|
||||
|
||||
// assumes the view's viewport is filled with lines
|
||||
func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) {
|
||||
view := self.getView()
|
||||
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver {
|
||||
view := self.getView()
|
||||
|
||||
@@ -515,6 +539,7 @@ func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
|
||||
self.Press(self.t.keys.Universal.StartSearch).
|
||||
Tap(func() {
|
||||
self.t.ExpectSearch().
|
||||
Clear().
|
||||
Type(text).
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -26,13 +26,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
Press(keys.Branches.CreateTag)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -23,13 +23,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Commits.CreateTag)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -5,29 +5,58 @@ import (
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// Originally we only suggested authors present in the current branch, but now
|
||||
// we include authors from other branches whose commits you've looked at in the
|
||||
// lazygit session.
|
||||
|
||||
var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Set author on a commit",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("original")
|
||||
|
||||
shell.SetConfig("user.email", "Bill@example.com")
|
||||
shell.SetConfig("user.name", "Bill Smith")
|
||||
|
||||
shell.EmptyCommit("one")
|
||||
|
||||
shell.NewBranch("other")
|
||||
|
||||
shell.SetConfig("user.email", "John@example.com")
|
||||
shell.SetConfig("user.name", "John Smith")
|
||||
|
||||
shell.EmptyCommit("two")
|
||||
|
||||
shell.Checkout("original")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("BS").Contains("one").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("original").IsSelected(),
|
||||
Contains("other"),
|
||||
).
|
||||
NavigateToLine(Contains("other")).
|
||||
PressEnter()
|
||||
|
||||
// ensuring we get these commit authors as suggestions
|
||||
t.Views().SubCommits().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("JS").Contains("two").IsSelected(),
|
||||
Contains("BS").Contains("one"),
|
||||
).
|
||||
)
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Press(keys.Commits.ResetCommitAuthor).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
@@ -38,14 +67,13 @@ var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Contains("Set author")).
|
||||
SuggestionLines(
|
||||
Contains("John Smith"),
|
||||
Contains("Bill Smith"),
|
||||
Contains("John Smith"),
|
||||
).
|
||||
ConfirmSuggestion(Contains("John Smith"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("JS").Contains("two").IsSelected(),
|
||||
Contains("BS").Contains("one"),
|
||||
Contains("JS").Contains("one").IsSelected(),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package filter_and_search
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FilterFuzzy = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Verify that fuzzy filtering works (not just exact matches)",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("this-is-my-branch")
|
||||
shell.EmptyCommit("first commit")
|
||||
shell.NewBranch("other-branch")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains(`other-branch`).IsSelected(),
|
||||
Contains(`this-is-my-branch`),
|
||||
).
|
||||
FilterOrSearch("timb"). // using first letters of words
|
||||
Lines(
|
||||
Contains(`this-is-my-branch`).IsSelected(),
|
||||
).
|
||||
FilterOrSearch("brnch"). // allows missing letter
|
||||
Lines(
|
||||
Contains(`other-branch`).IsSelected(),
|
||||
Contains(`this-is-my-branch`),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var CreateWhileCommitting = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Draft a commit message, escape out, and make a tag. Verify the draft message doesn't appear in the tag create prompt",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
shell.CreateFileAndAdd("file.txt", "file contents")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
Press(keys.Files.CommitChanges).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Commit summary")).
|
||||
Type("draft message").
|
||||
Cancel()
|
||||
})
|
||||
|
||||
t.Views().Tags().
|
||||
Focus().
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
InitialText(Equals(""))
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -19,19 +19,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Annotated")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag message:")).
|
||||
SwitchToDescription().
|
||||
Title(Equals("Tag description")).
|
||||
Type("message").
|
||||
SwitchToSummary().
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
@@ -44,6 +38,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Content(Equals("Are you sure you want to delete tag 'new-tag'?")).
|
||||
Confirm()
|
||||
}).
|
||||
IsEmpty()
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
// confirm content is cleared on next tag create
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
InitialText(Equals(""))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -19,13 +19,8 @@ var CrudLightweight = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
}).
|
||||
|
||||
@@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{
|
||||
file.RememberCommitMessageAfterFail,
|
||||
filter_and_search.FilterCommitFiles,
|
||||
filter_and_search.FilterFiles,
|
||||
filter_and_search.FilterFuzzy,
|
||||
filter_and_search.FilterMenu,
|
||||
filter_and_search.FilterRemoteBranches,
|
||||
filter_and_search.NestedFilter,
|
||||
@@ -206,10 +207,13 @@ var tests = []*components.IntegrationTest{
|
||||
sync.PushWithCredentialPrompt,
|
||||
sync.RenameBranchAndPull,
|
||||
tag.Checkout,
|
||||
tag.CreateWhileCommitting,
|
||||
tag.CrudAnnotated,
|
||||
tag.CrudLightweight,
|
||||
tag.Reset,
|
||||
ui.Accordion,
|
||||
ui.DoublePopup,
|
||||
ui.EmptyMenu,
|
||||
ui.SwitchTabFromMenu,
|
||||
undo.UndoCheckoutAndDrop,
|
||||
undo.UndoDrop,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// When in acccordion mode, Lazygit looks like this:
|
||||
//
|
||||
// ╶─Status─────────────────────────╴┌─Patch──────────────────────────────────────────────────────────┐
|
||||
// ╶─Files - Submodules──────0 of 0─╴│commit 6e56dd04b70e548976f7f2928c4d9c359574e2bc ▲
|
||||
// ╶─Local branches - Remotes1 of 1─╴│Author: CI <CI@example.com> █
|
||||
// ┌─Commits - Reflog───────────────┐│Date: Wed Jul 19 22:00:03 2023 +1000 │
|
||||
// │7fe02805 CI commit 12 ▲│ ▼
|
||||
// │6e56dd04 CI commit 11 █└────────────────────────────────────────────────────────────────┘
|
||||
// │a35c687d CI commit 10 ▼┌─Command log────────────────────────────────────────────────────┐
|
||||
// └───────────────────────10 of 20─┘│Random tip: To filter commits by path, press '<c-s>' │
|
||||
// ╶─Stash───────────────────0 of 0─╴└────────────────────────────────────────────────────────────────┘
|
||||
// <pgup>/<pgdown>: Scroll, <esc>: Cancel, q: Quit, ?: Keybindings, 1-Donate Ask Question unversioned
|
||||
|
||||
var Accordion = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Verify accordion mode kicks in when the screen height is too small",
|
||||
ExtraCmdArgs: []string{},
|
||||
Width: 100,
|
||||
Height: 10,
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateNCommits(20)
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
VisibleLines(
|
||||
Contains("commit 20").IsSelected(),
|
||||
Contains("commit 19"),
|
||||
Contains("commit 18"),
|
||||
).
|
||||
// go past commit 11, then come back, so that it ends up in the centre of the viewport
|
||||
NavigateToLine(Contains("commit 11")).
|
||||
NavigateToLine(Contains("commit 10")).
|
||||
NavigateToLine(Contains("commit 11")).
|
||||
VisibleLines(
|
||||
Contains("commit 12"),
|
||||
Contains("commit 11").IsSelected(),
|
||||
Contains("commit 10"),
|
||||
)
|
||||
|
||||
t.Views().Files().
|
||||
Focus()
|
||||
|
||||
// ensure we retain the same viewport upon re-focus
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
VisibleLines(
|
||||
Contains("commit 12"),
|
||||
Contains("commit 11").IsSelected(),
|
||||
Contains("commit 10"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Verify that we don't crash on an empty menu",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Press(keys.Universal.OptionMenu)
|
||||
|
||||
t.Views().Menu().
|
||||
IsFocused().
|
||||
// a string that filters everything out
|
||||
FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd").
|
||||
IsEmpty().
|
||||
Press(keys.Universal.Select)
|
||||
|
||||
// back in the files view, selecting the non-existing menu item was a no-op
|
||||
t.Views().Files().
|
||||
IsFocused()
|
||||
},
|
||||
})
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
type IntegrationTest interface {
|
||||
Run(GuiDriver)
|
||||
SetupConfig(config *config.AppConfig)
|
||||
RequiresHeadless() bool
|
||||
// width and height when running headless
|
||||
HeadlessDimensions() (int, int)
|
||||
}
|
||||
|
||||
// this is the interface through which our integration tests interact with the lazygit gui
|
||||
|
||||
+30
-3
@@ -164,6 +164,29 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
lineChan := make(chan []byte)
|
||||
lineWrittenChan := make(chan struct{})
|
||||
|
||||
// We're reading from the scanner in a separate goroutine because on windows
|
||||
// if running git through a shim, we sometimes kill the parent process without
|
||||
// killing its children, meaning the scanner blocks forever. This solution
|
||||
// leaves us with a dead goroutine, but it's better than blocking all
|
||||
// rendering to main views.
|
||||
go utils.Safe(func() {
|
||||
defer close(lineChan)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-opts.Stop:
|
||||
return
|
||||
case lineChan <- scanner.Bytes():
|
||||
// We need to confirm the data has been fed into the view before we
|
||||
// pull more from the scanner because the scanner uses the same backing
|
||||
// array and we don't want to be mutating that while it's being written
|
||||
<-lineWrittenChan
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
loaded := false
|
||||
|
||||
go utils.Safe(func() {
|
||||
@@ -203,13 +226,15 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
break outer
|
||||
case linesToRead := <-self.readLines:
|
||||
for i := 0; i < linesToRead.Total; i++ {
|
||||
var ok bool
|
||||
var line []byte
|
||||
select {
|
||||
case <-opts.Stop:
|
||||
break outer
|
||||
default:
|
||||
case line, ok = <-lineChan:
|
||||
break
|
||||
}
|
||||
|
||||
ok := scanner.Scan()
|
||||
loadingMutex.Lock()
|
||||
if !loaded {
|
||||
self.beforeStart()
|
||||
@@ -226,7 +251,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
self.onEndOfInput()
|
||||
break outer
|
||||
}
|
||||
writeToView(append(scanner.Bytes(), '\n'))
|
||||
writeToView(append(line, '\n'))
|
||||
lineWrittenChan <- struct{}{}
|
||||
|
||||
if i+1 == linesToRead.InitialRefreshAfter {
|
||||
// We have read enough lines to fill the view, so do a first refresh
|
||||
@@ -253,6 +279,7 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
|
||||
onDone()
|
||||
|
||||
close(done)
|
||||
close(lineWrittenChan)
|
||||
})
|
||||
|
||||
self.readLines <- linesToRead
|
||||
|
||||
+3
-1
@@ -31,7 +31,9 @@ var periods = []period{
|
||||
{"h", SECONDS_IN_HOUR},
|
||||
{"d", SECONDS_IN_DAY},
|
||||
{"w", SECONDS_IN_WEEK},
|
||||
{"M", SECONDS_IN_MONTH},
|
||||
// we're using 'm' for both minutes and months which is ambiguous but
|
||||
// disambiguating with another character feels like overkill.
|
||||
{"m", SECONDS_IN_MONTH},
|
||||
{"y", SECONDS_IN_YEAR},
|
||||
}
|
||||
|
||||
|
||||
+21
-8
@@ -177,13 +177,26 @@ type Gui struct {
|
||||
taskManager *TaskManager
|
||||
}
|
||||
|
||||
type NewGuiOpts struct {
|
||||
OutputMode OutputMode
|
||||
SupportOverlaps bool
|
||||
PlayRecording bool
|
||||
Headless bool
|
||||
// only applicable when Headless is true
|
||||
Width int
|
||||
// only applicable when Headless is true
|
||||
Height int
|
||||
|
||||
RuneReplacements map[rune]string
|
||||
}
|
||||
|
||||
// NewGui returns a new Gui object with a given output mode.
|
||||
func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless bool, runeReplacements map[rune]string) (*Gui, error) {
|
||||
func NewGui(opts NewGuiOpts) (*Gui, error) {
|
||||
g := &Gui{}
|
||||
|
||||
var err error
|
||||
if headless {
|
||||
err = g.tcellInitSimulation()
|
||||
if opts.Headless {
|
||||
err = g.tcellInitSimulation(opts.Width, opts.Height)
|
||||
} else {
|
||||
err = g.tcellInit(runeReplacements)
|
||||
}
|
||||
@@ -191,7 +204,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if headless || runtime.GOOS == "windows" {
|
||||
if opts.Headless || runtime.GOOS == "windows" {
|
||||
g.maxX, g.maxY = g.screen.Size()
|
||||
} else {
|
||||
// TODO: find out if we actually need this bespoke logic for linux
|
||||
@@ -201,7 +214,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless
|
||||
}
|
||||
}
|
||||
|
||||
g.outputMode = mode
|
||||
g.outputMode = opts.OutputMode
|
||||
|
||||
g.stop = make(chan struct{})
|
||||
|
||||
@@ -209,7 +222,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless
|
||||
g.userEvents = make(chan userEvent, 20)
|
||||
g.taskManager = newTaskManager()
|
||||
|
||||
if playRecording {
|
||||
if opts.PlayRecording {
|
||||
g.ReplayedEvents = replayedEvents{
|
||||
Keys: make(chan *TcellKeyEventWrapper),
|
||||
Resizes: make(chan *TcellResizeEventWrapper),
|
||||
@@ -221,14 +234,14 @@ func NewGui(mode OutputMode, supportOverlaps bool, playRecording bool, headless
|
||||
|
||||
// SupportOverlaps is true when we allow for view edges to overlap with other
|
||||
// view edges
|
||||
g.SupportOverlaps = supportOverlaps
|
||||
g.SupportOverlaps = opts.SupportOverlaps
|
||||
|
||||
// default keys for when searching strings in a view
|
||||
g.SearchEscapeKey = KeyEsc
|
||||
g.NextSearchMatchKey = 'n'
|
||||
g.PrevSearchMatchKey = 'N'
|
||||
|
||||
g.playRecording = playRecording
|
||||
g.playRecording = opts.PlayRecording
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ func registerRuneFallbacks(s tcell.Screen, additional map[rune]string) {
|
||||
}
|
||||
|
||||
// tcellInitSimulation initializes tcell screen for use.
|
||||
func (g *Gui) tcellInitSimulation() error {
|
||||
func (g *Gui) tcellInitSimulation(width int, height int) error {
|
||||
s := tcell.NewSimulationScreen("")
|
||||
if e := s.Init(); e != nil {
|
||||
return e
|
||||
@@ -90,7 +90,7 @@ func (g *Gui) tcellInitSimulation() error {
|
||||
Screen = s
|
||||
// setting to a larger value than the typical terminal size
|
||||
// so that during a test we're more likely to see an item to select in a view.
|
||||
s.SetSize(100, 100)
|
||||
s.SetSize(width, height)
|
||||
s.Sync()
|
||||
return nil
|
||||
}
|
||||
|
||||
+43
-5
@@ -269,7 +269,7 @@ func (v *View) FocusPoint(cx int, cy int) {
|
||||
_, height := v.Size()
|
||||
|
||||
ly := height - 1
|
||||
if ly == -1 {
|
||||
if ly < 0 {
|
||||
ly = 0
|
||||
}
|
||||
|
||||
@@ -1265,14 +1265,52 @@ func lineWrap(line []cell, columns int) [][]cell {
|
||||
|
||||
var n int
|
||||
var offset int
|
||||
lastWhitespaceIndex := -1
|
||||
lines := make([][]cell, 0, 1)
|
||||
for i := range line {
|
||||
rw := runewidth.RuneWidth(line[i].chr)
|
||||
currChr := line[i].chr
|
||||
rw := runewidth.RuneWidth(currChr)
|
||||
n += rw
|
||||
// if currChr == 'g' {
|
||||
// panic(n)
|
||||
// }
|
||||
if n > columns {
|
||||
n = rw
|
||||
lines = append(lines, line[offset:i])
|
||||
offset = i
|
||||
// This code is convoluted but we've got comprehensive tests so feel free to do whatever you want
|
||||
// to the code to simplify it so long as our tests still pass.
|
||||
if currChr == ' ' {
|
||||
// if the line ends in a space, we'll omit it. This means there'll be no
|
||||
// way to distinguish between a clean break and a mid-word break, but
|
||||
// I think it's worth it.
|
||||
lines = append(lines, line[offset:i])
|
||||
offset = i + 1
|
||||
n = 0
|
||||
} else if currChr == '-' {
|
||||
// if the last character is hyphen and the width of line is equal to the columns
|
||||
lines = append(lines, line[offset:i])
|
||||
offset = i
|
||||
n = rw
|
||||
} 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])
|
||||
offset = lastWhitespaceIndex + 1
|
||||
n = i - offset + 1
|
||||
}
|
||||
} else {
|
||||
// in this case we're breaking mid-word
|
||||
lines = append(lines, line[offset:i])
|
||||
offset = i
|
||||
n = rw
|
||||
}
|
||||
lastWhitespaceIndex = -1
|
||||
} else if line[i].chr == ' ' || line[i].chr == '-' {
|
||||
lastWhitespaceIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
|
||||
Reference in New Issue
Block a user