Compare commits

..

20 Commits

Author SHA1 Message Date
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
58 changed files with 356 additions and 967 deletions

26
.github/release.yml vendored Normal file
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:
- "*"

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:

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
.gitignore vendored
View File

@@ -39,5 +39,3 @@ test/results/**
oryxBuildBinary
__debug_bin
.worktrees

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

File diff suppressed because one or more lines are too long

2
go.mod
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.20230719120401-398f4965241f
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

4
go.sum
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.20230719120401-398f4965241f h1:w/pxI34XepTAx4HwxUu8ipimbVRgSTS+7ahmgFQwH80=
github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f/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=

View File

@@ -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]

View File

@@ -37,7 +37,6 @@ type GitCommand struct {
Tag *git_commands.TagCommands
WorkingTree *git_commands.WorkingTreeCommands
Bisect *git_commands.BisectCommands
Worktree *git_commands.WorktreeCommands
Loaders Loaders
}
@@ -51,7 +50,6 @@ type Loaders struct {
RemoteLoader *git_commands.RemoteLoader
StashLoader *git_commands.StashLoader
TagLoader *git_commands.TagLoader
Worktrees *git_commands.WorktreeLoader
}
func NewGitCommand(
@@ -129,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)
@@ -158,7 +154,6 @@ func NewGitCommandAux(
Tag: tagCommands,
Bisect: bisectCommands,
WorkingTree: workingTreeCommands,
Worktree: worktreeCommands,
Loaders: Loaders{
BranchLoader: branchLoader,
CommitFileLoader: commitFileLoader,
@@ -166,7 +161,6 @@ func NewGitCommandAux(
FileLoader: fileLoader,
ReflogCommitLoader: reflogCommitLoader,
RemoteLoader: remoteLoader,
Worktrees: worktreeLoader,
StashLoader: stashLoader,
TagLoader: tagLoader,
},

View File

@@ -2,7 +2,6 @@ package git_commands
import (
"fmt"
"os"
"regexp"
"strings"
@@ -118,11 +117,6 @@ outer:
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
currentDir, err := os.Getwd()
if err != nil {
panic(err)
}
output, err := self.getRawBranches()
if err != nil {
panic(err)
@@ -144,7 +138,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
return nil, false
}
return obtainBranch(split, currentDir), true
return obtainBranch(split), true
})
}
@@ -172,32 +166,28 @@ var branchFields = []string{
"upstream:track",
"subject",
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
"worktreepath",
}
// Obtain branch information from parsed line output of getRawBranches()
func obtainBranch(split []string, currentDir string) *models.Branch {
func obtainBranch(split []string) *models.Branch {
headMarker := split[0]
fullName := split[1]
upstreamName := split[2]
track := split[3]
subject := split[4]
commitHash := split[5]
branchDir := split[6]
checkedOutByOtherWorktree := len(branchDir) > 0 && branchDir != currentDir
name := strings.TrimPrefix(fullName, "heads/")
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
return &models.Branch{
Name: name,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
CheckedOutByOtherWorktree: checkedOutByOtherWorktree,
Name: name,
Pushables: pushables,
Pullables: pullables,
UpstreamGone: gone,
Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
}
}

View File

@@ -81,7 +81,7 @@ func TestObtainBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
branch := obtainBranch(s.input, "current-dir")
branch := obtainBranch(s.input)
assert.EqualValues(t, s.expectedBranch, branch)
})
}

View File

@@ -1,62 +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,
}
}
func (self *WorktreeCommands) New(worktreePath string, committish string) error {
cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath, committish).ToArgv()
return self.cmd.New(cmdArgs).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) IsCurrentWorktree(w *models.Worktree) bool {
return IsCurrentWorktree(w)
}
func IsCurrentWorktree(w *models.Worktree) bool {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
return EqualPath(pwd, w.Path)
}
func (self *WorktreeCommands) IsWorktreePathMissing(w *models.Worktree) bool {
if _, err := os.Stat(w.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", 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
}

View File

@@ -1,178 +0,0 @@
package git_commands
import (
"os"
"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
}
}
return worktrees, nil
}
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:], "/")
}

View File

@@ -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)
}
}

View File

@@ -26,8 +26,6 @@ type Branch struct {
Subject string
// commit hash
CommitHash string
CheckedOutByOtherWorktree bool
}
func (b *Branch) FullRefName() string {

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
}

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))

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
}

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,

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()

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
}

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,

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(
c.Model().Worktrees,
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()
}

View File

@@ -45,18 +45,7 @@ func (gui *Gui) resetHelpersAndControllers() {
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
stagingHelper := helpers.NewStagingHelper(helperCommon)
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper)
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,
@@ -95,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,
@@ -110,8 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() {
modeHelper,
appStatusHelper,
),
Search: helpers.NewSearchHelper(helperCommon),
Worktree: worktreeHelper,
Search: helpers.NewSearchHelper(helperCommon),
}
gui.CustomCommandsClient = custom_commands.NewClient(
@@ -150,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)
@@ -190,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,
@@ -312,10 +298,6 @@ func (gui *Gui) resetHelpersAndControllers() {
remotesController,
)
controllers.AttachControllers(gui.State.Contexts.Worktrees,
worktreesController,
)
controllers.AttachControllers(gui.State.Contexts.Stash,
stashController,
)

View File

@@ -202,37 +202,10 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch)
}
if selectedBranch.CheckedOutByOtherWorktree {
worktreeForRef, ok := self.worktreeForRef(selectedBranch.Name)
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef) {
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) worktreeForRef(ref string) (*models.Worktree, bool) {
for _, worktree := range self.c.Model().Worktrees {
if worktree.Branch == ref {
return worktree, true
}
}
return nil, false
}
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, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
})
}
func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error {
return self.createPullRequest(selectedBranch.Name, "")
}

View File

@@ -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)
}

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{},
}
}

View File

@@ -27,7 +27,6 @@ type RefreshHelper struct {
patchBuildingHelper *PatchBuildingHelper
stagingHelper *StagingHelper
mergeConflictsHelper *MergeConflictsHelper
worktreeHelper *WorktreeHelper
fileWatcher types.IFileWatcher
}
@@ -38,7 +37,6 @@ func NewRefreshHelper(
patchBuildingHelper *PatchBuildingHelper,
stagingHelper *StagingHelper,
mergeConflictsHelper *MergeConflictsHelper,
worktreeHelper *WorktreeHelper,
fileWatcher types.IFileWatcher,
) *RefreshHelper {
return &RefreshHelper{
@@ -48,7 +46,6 @@ func NewRefreshHelper(
patchBuildingHelper: patchBuildingHelper,
stagingHelper: stagingHelper,
mergeConflictsHelper: mergeConflictsHelper,
worktreeHelper: worktreeHelper,
fileWatcher: fileWatcher,
}
}
@@ -80,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,
@@ -132,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{}) })
}
@@ -178,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",
@@ -566,17 +557,6 @@ func (self *RefreshHelper) refreshRemotes() error {
return nil
}
func (self *RefreshHelper) refreshWorktrees() error {
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())
@@ -607,10 +587,6 @@ func (self *RefreshHelper) refreshStatus() {
name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name)
repoName := utils.GetCurrentRepoName()
mainWorktreeName := self.worktreeHelper.GetMainWorktreeName()
if repoName != mainWorktreeName {
repoName = fmt.Sprintf("%s(%s)", mainWorktreeName, style.FgBlue.Sprint(repoName))
}
status += fmt.Sprintf("%s → %s ", repoName, name)
self.c.SetViewContent(self.c.Views().Status, status)

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,11 +137,7 @@ 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 {
@@ -151,7 +146,7 @@ func (self *ReposHelper) DispatchSwitchTo(path string, reuse bool, errMsg string
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
}
@@ -176,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)
}

View File

@@ -1,93 +0,0 @@
package helpers
import (
"errors"
"fmt"
"io/fs"
"log"
"os"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type IWorktreeHelper interface {
GetMainWorktreeName() string
GetCurrentWorktreeName() string
}
type WorktreeHelper struct {
c *HelperCommon
reposHelper *ReposHelper
}
func NewWorktreeHelper(c *HelperCommon, reposHelper *ReposHelper) *WorktreeHelper {
return &WorktreeHelper{
c: c,
reposHelper: reposHelper,
}
}
func (self *WorktreeHelper) GetMainWorktreeName() string {
for _, worktree := range self.c.Model().Worktrees {
if worktree.Main() {
return worktree.Name()
}
}
return ""
}
func (self *WorktreeHelper) IsCurrentWorktree(w *models.Worktree) bool {
pwd, err := os.Getwd()
if err != nil {
log.Fatalln(err.Error())
}
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
}
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", w.Path, err).Error())
}
return false
}
func (self *WorktreeHelper) NewWorktree() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreePath,
HandleConfirm: func(path string) error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreePath,
HandleConfirm: func(committish string) 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(sanitizedBranchName(path), committish); err != nil {
return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
})
},
})
},
})
}
func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error {
if self.c.Git().Worktree.IsCurrentWorktree(worktree) {
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
}
self.c.LogAction(self.c.Tr.SwitchToWorktree)
// 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.reposHelper.DispatchSwitchTo(worktree.Path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
}

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
}
}

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},
})
}

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 {

View File

@@ -1,166 +0,0 @@
package controllers
import (
"fmt"
"strings"
"text/tabwriter"
"github.com/jesseduffield/gocui"
"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"
"github.com/jesseduffield/lazygit/pkg/utils"
)
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,
},
}
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) {
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) {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
}
return self.removeWithForce(worktree, false)
}
func (self *WorktreesController) removeWithForce(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.RemovingWorktree)
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
errMessage := err.Error()
if !force {
return self.removeWithForce(worktree, true)
}
return self.c.ErrorMsg(errMessage)
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES}})
})
},
})
}
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, context.WORKTREES_CONTEXT_KEY)
}
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
}

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
}
@@ -469,7 +460,8 @@ func NewGui(
PopupMutex: &deadlock.Mutex{},
PtyMutex: &deadlock.Mutex{},
},
InitialDir: initialDir,
InitialDir: initialDir,
afterLayoutFuncs: make(chan func() error, 1000),
}
gui.WatchFilesForChanges()
@@ -530,9 +522,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
}
@@ -555,10 +567,6 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
Tab: gui.c.Tr.TagsTitle,
ViewName: "tags",
},
{
Tab: gui.c.Tr.WorktreesTitle,
ViewName: "worktrees",
},
},
"commits": {
{
@@ -629,7 +637,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
}

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")
}
}

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) {

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

View File

@@ -12,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)
@@ -50,10 +49,6 @@ func getBranchDisplayStrings(
coloredName := nameTextStyle.Sprint(displayName)
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft)
if b.CheckedOutByOtherWorktree {
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, "(worktree)")
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon))
}
coloredName = fmt.Sprintf("%s %s", coloredName, branchStatus)
recencyColor := style.FgCyan
@@ -63,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)))
}

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
}

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(*models.Worktree) bool, isMissing func(*models.Worktree) bool) [][]string {
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
return GetWorktreeDisplayString(
isCurrent(worktree),
isMissing(worktree),
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
}

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

View File

@@ -13,7 +13,6 @@ const (
REFLOG
TAGS
REMOTES
WORKTREES
STATUS
SUBMODULES
STAGING

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

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

View File

@@ -480,7 +480,6 @@ type TranslationSet struct {
ErrCannotEditDirectory string
ErrStageDirWithInlineMergeConflicts string
ErrRepositoryMovedOrDeleted string
ErrWorktreeMovedOrRemoved string
CommandLog string
ToggleShowCommandLog string
FocusCommandLog string
@@ -541,27 +540,6 @@ type TranslationSet struct {
FilterPrefix string
ExitSearchMode string
ExitTextFilterMode string
SwitchToWorktree string
RemoveWorktree string
RemoveWorktreeTitle 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
NewWorktreeBranch string
Name string
Branch string
Path string
Actions Actions
Bisect Bisect
}
@@ -688,8 +666,6 @@ type Actions struct {
ResetBisect string
BisectSkip string
BisectMark string
RemoveWorktree string
AddWorktree string
}
const englishIntroPopupMessage = `
@@ -1199,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",
@@ -1258,27 +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}}' is not fully merged. Are you sure you want to remove it?",
RemovingWorktree: "Deleting 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",
NewWorktreeBranch: "New worktree branch (leave blank to use the current branch)",
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",
@@ -1386,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",

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)
}

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)

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()

View File

@@ -209,7 +209,9 @@ var tests = []*components.IntegrationTest{
tag.CrudAnnotated,
tag.CrudLightweight,
tag.Reset,
ui.Accordion,
ui.DoublePopup,
ui.EmptyMenu,
ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop,
undo.UndoDrop,

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"),
)
},
})

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()
},
})

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

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
}

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
}

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
}

2
vendor/modules.txt vendored
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.20230719120401-398f4965241f
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10