Compare commits

..
33 Commits
Author SHA1 Message Date
Jesse Duffield 1b05ba252c Fix crash caused by simultaneous read/write of scanner buffer (#2813) 2023-07-23 13:55:51 +10:00
Jesse Duffield 28474b08ee Fix crash caused by simultaneous read/write of scanner buffer 2023-07-23 13:17:37 +10:00
Jesse Duffield 6f13b42279 Better word wrap (#2812) 2023-07-23 11:46:52 +10:00
Jesse Duffield 8637587b82 Better word wrap
Word wrapping has been pretty bad so far so let's fix that.
2023-07-23 11:43:10 +10:00
Jesse Duffield f581dc4a56 Update README.md 2023-07-22 15:31:03 +10:00
Jesse Duffield 693e9fc152 Update README.md
Removing unneeded go docs tag
2023-07-22 15:14:18 +10:00
Jesse Duffield c595833883 Better tag creation UX (#2809) 2023-07-22 14:44:18 +10:00
Jesse Duffield 7807b40322 Better tag creation UX
Previously we used a single-line prompt for a tag annotation. Now we're using the commit message
prompt.

I've had to update other uses of that prompt to allow the summary and description labels to
be passed in
2023-07-22 14:36:35 +10:00
Jesse Duffield b284970bac Use fuzzy search when filtering a view (#2808) 2023-07-22 13:17:46 +10:00
Jesse Duffield b46623ebef Use fuzzy search when filtering a view
This adds fuzzy filtering instead of exact match filtering, which is more forgiving of typos
and allows more efficiency.
2023-07-22 13:14:29 +10:00
Jesse Duffield 084c0a19bc Include more commit authors in author suggestions (#2807) 2023-07-22 11:00:38 +10:00
Jesse Duffield 3cee37388c Keep track of authors across local commits and branch commits for suggestions
Previously, we would only show the authors based on local commits, but sometimes you want to set a commit author
to that of a commit on another branch. Now, so long as you've viewed the branch's commits, the author will appear
as a suggestion.
2023-07-22 10:47:04 +10:00
Andrew Savinykh a7969aef2c Fix rendering to main view on windows 2023-07-22 09:14:05 +10:00
Jesse Duffield 39c900c7e7 Fix goreleaser 2023-07-21 09:03:47 +10:00
Jesse Duffield 6e247c1583 Only apply right-alignment on first column of keybindings menu (#2801) 2023-07-20 21:27:01 +10:00
Jesse Duffield 87bf1dbc7f Only apply right-alignment on first column of keybindings menu
Previously we applied a right-align on the first column of _all_ menus, even though we really
only intended for it to be on the first column of the keybindings menu (that you get from pressing
'?')
2023-07-20 21:23:46 +10:00
Jesse Duffield 1f920ae6ba Fix crash on empty menu (#2799) 2023-07-20 21:17:14 +10:00
Jesse Duffield 932e01b41a Add test for crashing on empty menu 2023-07-20 21:08:56 +10:00
Jesse Duffield 373f24c80f Fix crash on empty menu
When a menu is empty (e.g. due to filtering) we shouldn't crash on focus or selection
2023-07-20 21:05:52 +10:00
Jesse Duffield a548b289ef Add missing label to label checker (#2798) 2023-07-20 17:33:10 +10:00
Jesse Duffield b168fc8cdd Add missing label to label checker 2023-07-20 17:31:40 +10:00
Jesse Duffield 94845dcf98 Update release notes config and add CI check (#2797) 2023-07-20 17:19:49 +10:00
Jesse Duffield 1af6dff64e Update release notes config and add CI check 2023-07-20 17:15:10 +10:00
Jesse Duffield 72de4f436e Add release config for generating release notes (#2793) 2023-07-19 23:38:04 +10:00
Jesse Duffield effda8291b Add release config for generating release notes
After going and adding labels for all of these I found out that 'improvement' should be 'enhancement' and 'bugfix' should be 'bug'
but I don't know how to bulk update them (and I can't rename because the desired labels already exist).

I'll work that out later, this is good enough for now
2023-07-19 23:37:34 +10:00
Jesse Duffield 7985e31020 Properly fix accordion issue (#2792) 2023-07-19 22:24:42 +10:00
Jesse Duffield 866e0a618b Add integration test for accordion mode 2023-07-19 22:17:29 +10:00
Jesse Duffield a5ee61c117 Properly fix accordion issue
The true issue was that we were focusing the line in the view before it gets resized in the layout function.
This meant if the view was squashed in accordion mode, the view wouldn't know how to set the cursor/origin to
focus the line.

Now we've got a queue of 'after layout' functions i.e. functions to call at the end of the layout function,
right before views are drawn.

The only caveat is that we can't have an infinite buffer so we're arbitrarily capping it at 1000 and dropping
functions if we exceed that limit. But that really should never happen.
2023-07-19 21:16:27 +10:00
Jesse Duffield 08aad924d7 Fix accordion issue (#2791) 2023-07-19 21:07:25 +10:00
Jesse Duffield be02786dad Fix accordion issue
This fixes the issue in accordion mode where the current line wasn't in the viewport upon focus.

It doesn't perfectly fix it: the current line always appears at the top of the view. But it's good enough
to cut a new release. The proper fix is to only focus the line after the view has had its height adjusted.
2023-07-19 20:39:10 +10:00
README-bot 949022db8c Updated README.md 2023-07-19 08:19:33 +00:00
Stefan Haller b1a090d4c0 Fix crash when a background fetch prompts for credentials (#2789) 2023-07-19 10:19:18 +02:00
Stefan Haller 39f3f150ed Fix crash when a background fetch prompts for credentials
This happens consistently for my when I close my MacBook's lid. It seems that
MacOS locks the user's keychain in this case, and since I have my keychain
provide the pass phrases for my ssh keys, fetching fails because it tries to
prompt me for a pass phrase.

This all worked correctly already, we have the FailOnCredentialRequest()
mechanism specifically for this situation, so all is great. The only problem was
that it was trying to pause the ongoing task while prompting the user for input;
but the task is nil for a background fetch (and should be).
2023-07-18 18:53:35 +02:00
95 changed files with 819 additions and 1699 deletions
+26
View File
@@ -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:
- "*"
+4
View File
@@ -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:
+8
View File
@@ -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"
-2
View File
@@ -39,5 +39,3 @@ test/results/**
oryxBuildBinary
__debug_bin
.worktrees
+1 -1
View File
@@ -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
View File
@@ -1 +0,0 @@
eUTlaNqeTB
-1
View File
@@ -1 +0,0 @@
qtuVMihtEl
+17 -16
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+1 -2
View File
@@ -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]
-8
View File
@@ -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,
},
+7 -8
View File
@@ -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)
+17 -12
View File
@@ -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...)
}
+1 -1
View File
@@ -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
}
+3 -3
View File
@@ -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
}
-4
View File
@@ -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)
}
-107
View File
@@ -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)
}
}
+13
View File
@@ -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)
}
-31
View File
@@ -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
}
-9
View File
@@ -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
+6 -2
View File
@@ -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))
-8
View File
@@ -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",
-13
View File
@@ -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
}
-1
View File
@@ -31,7 +31,6 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
c.Modes().Diffing.Ref,
c.Tr,
c.UserConfig,
c.Model().Worktrees,
)
}
+12 -5
View File
@@ -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() {
-7
View File
@@ -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,
+27 -12
View File
@@ -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 -3
View File
@@ -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()
+13 -7
View File
@@ -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
}
-1
View File
@@ -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,
-56
View File
@@ -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
View File
@@ -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,
)
+1 -62
View File
@@ -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(
+27 -17
View File
@@ -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)
}
-2
View File
@@ -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{},
}
}
+29 -31
View File
@@ -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)
}
+6 -13
View File
@@ -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)
}
+38 -61
View File
@@ -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 {
+3 -1
View File
@@ -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
}
}
+5 -3
View File
@@ -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},
})
}
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
}
-139
View File
@@ -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
View File
@@ -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
}
+9 -4
View File
@@ -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
View File
@@ -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) {
+1 -1
View File
@@ -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
+1 -10
View File
@@ -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 -16
View File
@@ -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
}
-36
View File
@@ -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
}
-49
View File
@@ -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
View File
@@ -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
-1
View File
@@ -13,7 +13,6 @@ const (
REFLOG
TAGS
REMOTES
WORKTREES
STATUS
SUBMODULES
STAGING
-1
View File
@@ -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
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+3 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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: "Переключить коммит",
+3 -4
View File
@@ -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: "檢出提交",
+4 -1
View File
@@ -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
}
+26
View File
@@ -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)
+25
View File
@@ -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()
+2 -7
View File
@@ -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()
+2 -7
View File
@@ -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()
+32 -4
View File
@@ -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(""))
})
},
})
+13 -12
View File
@@ -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()
}).
+4
View File
@@ -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,
+61
View File
@@ -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"),
)
},
})
+31
View File
@@ -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()
},
})
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+1 -1
View File
@@ -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