Compare commits
45 Commits
test-m-key
...
v0.39.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b05ba252c | ||
|
|
28474b08ee | ||
|
|
6f13b42279 | ||
|
|
8637587b82 | ||
|
|
f581dc4a56 | ||
|
|
693e9fc152 | ||
|
|
c595833883 | ||
|
|
7807b40322 | ||
|
|
b284970bac | ||
|
|
b46623ebef | ||
|
|
084c0a19bc | ||
|
|
3cee37388c | ||
|
|
a7969aef2c | ||
|
|
39c900c7e7 | ||
|
|
6e247c1583 | ||
|
|
87bf1dbc7f | ||
|
|
1f920ae6ba | ||
|
|
932e01b41a | ||
|
|
373f24c80f | ||
|
|
a548b289ef | ||
|
|
b168fc8cdd | ||
|
|
94845dcf98 | ||
|
|
1af6dff64e | ||
|
|
72de4f436e | ||
|
|
effda8291b | ||
|
|
7985e31020 | ||
|
|
866e0a618b | ||
|
|
a5ee61c117 | ||
|
|
08aad924d7 | ||
|
|
be02786dad | ||
|
|
949022db8c | ||
|
|
b1a090d4c0 | ||
|
|
39f3f150ed | ||
|
|
7e9f669421 | ||
|
|
6b769fb138 | ||
|
|
cc835a813e | ||
|
|
6103a4d13c | ||
|
|
69575dd4f3 | ||
|
|
bfcff3222c | ||
|
|
5adea789d0 | ||
|
|
78bbdca757 | ||
|
|
5cb82a49f8 | ||
|
|
9617737352 | ||
|
|
a251f6ad6c | ||
|
|
b61ca21a84 |
26
.github/release.yml
vendored
Normal file
26
.github/release.yml
vendored
Normal 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
.github/workflows/cd.yml
vendored
4
.github/workflows/cd.yml
vendored
@@ -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
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -77,7 +77,6 @@ git:
|
||||
useConfig: false
|
||||
commit:
|
||||
signOff: false
|
||||
verbose: default # one of 'default' | 'always' | 'never'
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
|
||||
@@ -59,6 +59,12 @@ For a given custom command, here are the allowed fields:
|
||||
| description | Label for the custom command when displayed in the keybindings menu | no |
|
||||
| stream | Whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | Whether you want to show the command's output in a popup within Lazygit | no |
|
||||
| after | Actions to take after the command has completed | no |
|
||||
|
||||
Here are the options for the `after` key:
|
||||
| _field_ | _description_ | required |
|
||||
|-----------------|----------------------|-|
|
||||
| checkForConflicts | true/false. If true, check for merge conflicts | no |
|
||||
|
||||
## Contexts
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -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
|
||||
|
||||
4
go.sum
4
go.sum
@@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b h1:8FmmdaYHes1m3oNyNdS+VIgkgkFpNZAWuwTnvp0tG14=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144 h1:gwy5JzP6+PhcPFG1obkUSLGcTkUY88sLKlCPOFjwtak=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
|
||||
@@ -101,7 +101,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commitMessage": tr.CommitSummaryTitle,
|
||||
"commitDescription": tr.CommitDescriptionTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
|
||||
@@ -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)
|
||||
@@ -95,7 +94,6 @@ func (self *CommitCommands) commitMessageArgs(message string) []string {
|
||||
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
|
||||
cmdArgs := NewGitCmd("commit").
|
||||
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
|
||||
ArgIf(self.verboseFlag() != "", self.verboseFlag()).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs)
|
||||
@@ -109,17 +107,6 @@ func (self *CommitCommands) signoffFlag() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitCommands) verboseFlag() string {
|
||||
switch self.config.UserConfig.Git.Commit.Verbose {
|
||||
case "always":
|
||||
return "--verbose"
|
||||
case "never":
|
||||
return "--no-verbose"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
|
||||
cmdArgs := NewGitCmd("log").Arg("-1", "--pretty=%s").ToArgv()
|
||||
|
||||
@@ -162,11 +162,17 @@ func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
||||
tags := []string{}
|
||||
|
||||
if extraInfo != "" {
|
||||
re := regexp.MustCompile(`tag: ([^,\)]+)`)
|
||||
tagMatch := re.FindStringSubmatch(extraInfo)
|
||||
if len(tagMatch) > 1 {
|
||||
tags = append(tags, tagMatch[1])
|
||||
extraInfoFields := strings.Split(extraInfo, ",")
|
||||
for _, extraInfoField := range extraInfoFields {
|
||||
extraInfoField = strings.TrimSpace(extraInfoField)
|
||||
re := regexp.MustCompile(`tag: (.+)`)
|
||||
tagMatch := re.FindStringSubmatch(extraInfoField)
|
||||
if len(tagMatch) > 1 {
|
||||
tags = append(tags, tagMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
extraInfo = "(" + extraInfo + ")"
|
||||
}
|
||||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
@@ -585,4 +591,4 @@ func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
return self.cmd.New(cmdArgs).DontLog()
|
||||
}
|
||||
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s`
|
||||
const prettyFormat = `--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s`
|
||||
|
||||
@@ -14,16 +14,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com|origin/better-tests|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com|tag: 123, tag: 456|d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com|HEAD -> better-tests|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -45,7 +45,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -57,7 +57,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "refs/heads/mybranch", "mybranch@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "refs/heads/mybranch", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -72,7 +72,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches have an upstream
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil). // this one does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")). // this one doesn't, so it checks origin instead
|
||||
@@ -117,8 +117,8 @@ func TestGetCommits(t *testing.T) {
|
||||
Name: "refactor",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Tags: []string{"123", "456"},
|
||||
ExtraInfo: "(tag: 123, tag: 456)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640823749,
|
||||
@@ -209,7 +209,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "", errors.New("error")).
|
||||
ExpectGitArgs([]string{"rev-parse", "--verify", "--quiet", "refs/remotes/origin/master"}, "", errors.New("error")).
|
||||
@@ -246,7 +246,7 @@ func TestGetCommits(t *testing.T) {
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--topo-order", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "master@{u}"}, "refs/remotes/origin/master", nil).
|
||||
ExpectGitArgs([]string{"rev-parse", "--symbolic-full-name", "main@{u}"}, "", errors.New("error")).
|
||||
@@ -282,7 +282,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--no-show-signature", "--"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
@@ -294,7 +294,7 @@ func TestGetCommits(t *testing.T) {
|
||||
opts: GetCommitsOptions{RefName: "HEAD", FilterPath: "src"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"merge-base", "HEAD", "HEAD@{u}"}, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
ExpectGitArgs([]string{"log", "HEAD", "--oneline", "--pretty=format:%H%x00%at%x00%aN%x00%ae%x00%D%x00%p%x00%s", "--abbrev=40", "--follow", "--no-show-signature", "--", "src"}, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
@@ -114,7 +119,6 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
configSignoff bool
|
||||
configVerbose string
|
||||
expected []string
|
||||
}
|
||||
|
||||
@@ -122,33 +126,13 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
{
|
||||
testName: "Commit using editor",
|
||||
configSignoff: false,
|
||||
configVerbose: "default",
|
||||
expected: []string{"commit"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "never",
|
||||
expected: []string{"commit", "--no-verbose"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "always",
|
||||
expected: []string{"commit", "--verbose"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff",
|
||||
configSignoff: true,
|
||||
configVerbose: "default",
|
||||
expected: []string{"commit", "--signoff"},
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff and --no-verbose",
|
||||
configSignoff: true,
|
||||
configVerbose: "never",
|
||||
expected: []string{"commit", "--signoff", "--no-verbose"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -156,7 +140,6 @@ func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
userConfig.Git.Commit.Verbose = s.configVerbose
|
||||
|
||||
runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner})
|
||||
|
||||
@@ -302,7 +302,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
|
||||
head_message, _ := self.commit.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
|
||||
if err := self.commit.CommitCmdObj(new_message, "").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ func NewRebaseCommands(
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return self.commit.RewordLastCommit(message)
|
||||
return self.commit.RewordLastCommit(summary, description)
|
||||
}
|
||||
|
||||
err := self.BeginInteractiveRebaseForCommit(commits, index, false)
|
||||
@@ -47,7 +47,7 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
|
||||
}
|
||||
|
||||
// now the selected commit should be our head so we'll amend it with the new message
|
||||
err = self.commit.RewordLastCommit(message)
|
||||
err = self.commit.RewordLastCommit(summary, description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
13
pkg/commands/models/author.go
Normal file
13
pkg/commands/models/author.go
Normal 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)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func TestCmdObjToString(t *testing.T) {
|
||||
@@ -31,3 +34,20 @@ func TestCmdObjToString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
task := gocui.NewFakeTask()
|
||||
cmdObj := &CmdObj{task: task, cmd: &exec.Cmd{}}
|
||||
clone := cmdObj.Clone()
|
||||
if clone == cmdObj {
|
||||
t.Errorf("Clone should not return the same object")
|
||||
}
|
||||
|
||||
if clone.GetTask() == nil {
|
||||
t.Errorf("Clone task should not be nil")
|
||||
}
|
||||
|
||||
if clone.GetTask() != task {
|
||||
t.Errorf("Clone should have the same task")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ type PagingConfig struct {
|
||||
}
|
||||
|
||||
type CommitConfig struct {
|
||||
SignOff bool `yaml:"signOff"`
|
||||
Verbose string `yaml:"verbose"`
|
||||
SignOff bool `yaml:"signOff"`
|
||||
}
|
||||
|
||||
type MergingConfig struct {
|
||||
@@ -349,16 +348,21 @@ type OSConfig struct {
|
||||
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
|
||||
}
|
||||
|
||||
type CustomCommandAfterHook struct {
|
||||
CheckForConflicts bool `yaml:"checkForConflicts"`
|
||||
}
|
||||
|
||||
type CustomCommand struct {
|
||||
Key string `yaml:"key"`
|
||||
Context string `yaml:"context"`
|
||||
Command string `yaml:"command"`
|
||||
Subprocess bool `yaml:"subprocess"`
|
||||
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
||||
LoadingText string `yaml:"loadingText"`
|
||||
Description string `yaml:"description"`
|
||||
Stream bool `yaml:"stream"`
|
||||
ShowOutput bool `yaml:"showOutput"`
|
||||
Key string `yaml:"key"`
|
||||
Context string `yaml:"context"`
|
||||
Command string `yaml:"command"`
|
||||
Subprocess bool `yaml:"subprocess"`
|
||||
Prompts []CustomCommandPrompt `yaml:"prompts"`
|
||||
LoadingText string `yaml:"loadingText"`
|
||||
Description string `yaml:"description"`
|
||||
Stream bool `yaml:"stream"`
|
||||
ShowOutput bool `yaml:"showOutput"`
|
||||
After CustomCommandAfterHook `yaml:"after"`
|
||||
}
|
||||
|
||||
type CustomCommandPrompt struct {
|
||||
@@ -445,7 +449,6 @@ func GetDefaultConfig() *UserConfig {
|
||||
},
|
||||
Commit: CommitConfig{
|
||||
SignOff: false,
|
||||
Verbose: "default",
|
||||
},
|
||||
Merging: MergingConfig{
|
||||
ManualCommit: false,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sahilm/fuzzy"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
@@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int {
|
||||
return len(self.getList())
|
||||
}
|
||||
|
||||
type fuzzySource[T any] struct {
|
||||
list []T
|
||||
getFilterFields func(T) []string
|
||||
}
|
||||
|
||||
var _ fuzzy.Source = &fuzzySource[string]{}
|
||||
|
||||
func (self *fuzzySource[T]) String(i int) string {
|
||||
return strings.Join(self.getFilterFields(self.list[i]), " ")
|
||||
}
|
||||
|
||||
func (self *fuzzySource[T]) Len() int {
|
||||
return len(self.list)
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) applyFilter() {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
@@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() {
|
||||
if self.filter == "" {
|
||||
self.filteredIndices = nil
|
||||
} else {
|
||||
self.filteredIndices = []int{}
|
||||
for i, item := range self.getList() {
|
||||
for _, field := range self.getFilterFields(item) {
|
||||
if self.match(field, self.filter) {
|
||||
self.filteredIndices = append(self.filteredIndices, i)
|
||||
break
|
||||
}
|
||||
}
|
||||
source := &fuzzySource[T]{
|
||||
list: self.getList(),
|
||||
getFilterFields: self.getFilterFields,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) match(haystack string, needle string) bool {
|
||||
return utils.CaseAwareContains(haystack, needle)
|
||||
matches := fuzzy.FindFrom(self.filter, source)
|
||||
self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
|
||||
return match.Index
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
|
||||
|
||||
@@ -14,7 +14,7 @@ type ListContextTrait struct {
|
||||
list types.IList
|
||||
getDisplayStrings func(startIdx int, length int) [][]string
|
||||
// Alignment for each column. If nil, the default is left alignment
|
||||
columnAlignments []utils.Alignment
|
||||
getColumnAlignments func() []utils.Alignment
|
||||
// Some contexts, like the commit context, will highlight the path from the selected commit
|
||||
// to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
|
||||
// so that we show the highlighted path.
|
||||
@@ -31,7 +31,14 @@ func (self *ListContextTrait) GetList() types.IList {
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) FocusLine() {
|
||||
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
|
||||
// Doing this at the end of the layout function because we need the view to be
|
||||
// resized before we focus the line, otherwise if we're in accordion mode
|
||||
// the view could be squashed and won't how to adjust the cursor/origin
|
||||
self.c.AfterLayout(func() error {
|
||||
self.GetViewTrait().FocusPoint(self.list.GetSelectedLineIdx())
|
||||
return nil
|
||||
})
|
||||
|
||||
self.setFooter()
|
||||
|
||||
if self.refreshViewportOnChange {
|
||||
@@ -75,9 +82,13 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
|
||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||
func (self *ListContextTrait) HandleRender() error {
|
||||
self.list.RefreshSelectedIdx()
|
||||
var columnAlignments []utils.Alignment
|
||||
if self.getColumnAlignments != nil {
|
||||
columnAlignments = self.getColumnAlignments()
|
||||
}
|
||||
content := utils.RenderDisplayStrings(
|
||||
self.getDisplayStrings(0, self.list.Len()),
|
||||
self.columnAlignments,
|
||||
columnAlignments,
|
||||
)
|
||||
self.GetViewTrait().SetContent(content)
|
||||
self.c.Render()
|
||||
|
||||
@@ -35,10 +35,10 @@ func NewMenuContext(
|
||||
Focusable: true,
|
||||
HasUncontrolledBounds: true,
|
||||
})),
|
||||
getDisplayStrings: viewModel.GetDisplayStrings,
|
||||
list: viewModel,
|
||||
c: c,
|
||||
columnAlignments: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
|
||||
getDisplayStrings: viewModel.GetDisplayStrings,
|
||||
list: viewModel,
|
||||
c: c,
|
||||
getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,9 @@ func (self *MenuContext) GetSelectedItemId() string {
|
||||
}
|
||||
|
||||
type MenuViewModel struct {
|
||||
c *ContextCommon
|
||||
menuItems []*types.MenuItem
|
||||
c *ContextCommon
|
||||
menuItems []*types.MenuItem
|
||||
columnAlignment []utils.Alignment
|
||||
*FilteredListViewModel[*types.MenuItem]
|
||||
}
|
||||
|
||||
@@ -73,8 +74,9 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) {
|
||||
func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment []utils.Alignment) {
|
||||
self.menuItems = items
|
||||
self.columnAlignment = columnAlignment
|
||||
}
|
||||
|
||||
// TODO: move into presentation package
|
||||
@@ -135,6 +137,10 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if selectedItem == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := selectedItem.OnPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,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,
|
||||
|
||||
@@ -395,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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -137,33 +137,47 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error {
|
||||
} else if strings.Contains(result.Error(), "No rebase in progress?") {
|
||||
// assume in this case that we're already done
|
||||
return nil
|
||||
} else if isMergeConflictErr(result.Error()) {
|
||||
mode := self.workingTreeStateNoun()
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.FoundConflictsTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.ViewConflictsMenuItem,
|
||||
OnPress: func() error {
|
||||
return self.c.PushContext(self.c.Contexts().Files)
|
||||
},
|
||||
Key: 'v',
|
||||
},
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode),
|
||||
OnPress: func() error {
|
||||
return self.genericMergeCommand(REBASE_OPTION_ABORT)
|
||||
},
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
HideCancel: true,
|
||||
})
|
||||
} else {
|
||||
return self.CheckForConflicts(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) CheckForConflicts(result error) error {
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isMergeConflictErr(result.Error()) {
|
||||
return self.PromptForConflictHandling()
|
||||
} else {
|
||||
return self.c.ErrorMsg(result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) PromptForConflictHandling() error {
|
||||
mode := self.workingTreeStateNoun()
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.FoundConflictsTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.ViewConflictsMenuItem,
|
||||
OnPress: func() error {
|
||||
return self.c.PushContext(self.c.Contexts().Files)
|
||||
},
|
||||
Key: 'v',
|
||||
},
|
||||
{
|
||||
Label: fmt.Sprintf(self.c.Tr.AbortMenuItem, mode),
|
||||
OnPress: func() error {
|
||||
return self.genericMergeCommand(REBASE_OPTION_ABORT)
|
||||
},
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
HideCancel: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error {
|
||||
// prompt user to confirm that they want to abort, then do it
|
||||
mode := self.workingTreeStateNoun()
|
||||
|
||||
@@ -266,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)
|
||||
@@ -287,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()
|
||||
|
||||
@@ -176,9 +176,11 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su
|
||||
}
|
||||
|
||||
func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
authors := lo.Uniq(slices.Map(self.c.Model().Commits, func(commit *models.Commit) string {
|
||||
return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail)
|
||||
}))
|
||||
authors := lo.Map(lo.Values(self.c.Model().Authors), func(author *models.Author, _ int) string {
|
||||
return author.Combined()
|
||||
})
|
||||
|
||||
slices.Sort(authors)
|
||||
|
||||
return FuzzySearchFunc(authors)
|
||||
}
|
||||
|
||||
@@ -1,77 +1,54 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// Helper structs are for defining functionality that could be used by multiple contexts.
|
||||
// For example, here we have a CreateTagMenu which is applicable to both the tags context
|
||||
// and the commits context.
|
||||
|
||||
type TagsHelper struct {
|
||||
c *HelperCommon
|
||||
c *HelperCommon
|
||||
commitsHelper *CommitsHelper
|
||||
}
|
||||
|
||||
func NewTagsHelper(c *HelperCommon) *TagsHelper {
|
||||
func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper) *TagsHelper {
|
||||
return &TagsHelper{
|
||||
c: c,
|
||||
c: c,
|
||||
commitsHelper: commitsHelper,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagsHelper) CreateTagMenu(ref string, onCreate func()) error {
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.TagMenuTitle,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.LightweightTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateLightweightTag(ref, onCreate)
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.AnnotatedTag,
|
||||
OnPress: func() error {
|
||||
return self.handleCreateAnnotatedTag(ref, onCreate)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) afterTagCreate(onCreate func()) error {
|
||||
onCreate()
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagMessageTitle,
|
||||
HandleConfirm: func(msg string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, msg); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.afterTagCreate(onCreate)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func()) error {
|
||||
return self.c.Prompt(types.PromptOpts{
|
||||
Title: self.c.Tr.TagNameTitle,
|
||||
HandleConfirm: func(tagName string) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
|
||||
if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
|
||||
return self.c.Error(err)
|
||||
func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error {
|
||||
onConfirm := func(tagName string, description string) error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.CreatingTag, func(gocui.Task) error {
|
||||
if description != "" {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
|
||||
if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
} else {
|
||||
self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
|
||||
if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
}
|
||||
return self.afterTagCreate(onCreate)
|
||||
|
||||
self.commitsHelper.OnCommitSuccess()
|
||||
|
||||
return self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return self.commitsHelper.OpenCommitMessagePanel(
|
||||
&OpenCommitMessagePanelOpts{
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: "",
|
||||
SummaryTitle: self.c.Tr.TagNameTitle,
|
||||
DescriptionTitle: self.c.Tr.TagMessageTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: onConfirm,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,19 +99,19 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin
|
||||
|
||||
return self.commitsHelper.OpenCommitMessagePanel(
|
||||
&OpenCommitMessagePanelOpts{
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: initialMessage,
|
||||
Title: self.c.Tr.CommitSummary,
|
||||
PreserveMessage: true,
|
||||
OnConfirm: self.handleCommit,
|
||||
CommitIndex: context.NoCommitIndex,
|
||||
InitialMessage: initialMessage,
|
||||
SummaryTitle: self.c.Tr.CommitSummaryTitle,
|
||||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: true,
|
||||
OnConfirm: self.handleCommit,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) handleCommit(message string) error {
|
||||
cmdObj := self.c.Git().Commit.CommitCmdObj(message)
|
||||
func (self *WorkingTreeHelper) handleCommit(summary string, description string) error {
|
||||
cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description)
|
||||
self.c.LogAction(self.c.Tr.Actions.Commit)
|
||||
_ = self.commitsHelper.PopCommitMessageContexts()
|
||||
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
|
||||
self.commitsHelper.OnCommitSuccess()
|
||||
return nil
|
||||
|
||||
@@ -267,22 +267,22 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
|
||||
return self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
CommitIndex: self.context().GetSelectedLineIdx(),
|
||||
InitialMessage: commitMessage,
|
||||
Title: self.c.Tr.Actions.RewordCommit,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: self.handleReword,
|
||||
CommitIndex: self.context().GetSelectedLineIdx(),
|
||||
InitialMessage: commitMessage,
|
||||
SummaryTitle: self.c.Tr.Actions.RewordCommit,
|
||||
DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
|
||||
PreserveMessage: false,
|
||||
OnConfirm: self.handleReword,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) handleReword(message string) error {
|
||||
err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), message)
|
||||
func (self *LocalCommitsController) handleReword(summary string, description string) error {
|
||||
err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), summary, description)
|
||||
if err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
self.c.Helpers().Commits.OnCommitSuccess()
|
||||
_ = self.c.Helpers().Commits.PopCommitMessageContexts()
|
||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
|
||||
}
|
||||
|
||||
@@ -682,7 +682,7 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
||||
return self.c.Helpers().Tags.CreateTagMenu(commit.Sha, func() {})
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Sha, func() {})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) openSearch() error {
|
||||
|
||||
@@ -53,7 +53,9 @@ func (self *MenuController) GetOnClick() func() error {
|
||||
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
|
||||
return func(types.OnFocusOpts) error {
|
||||
selectedMenuItem := self.context().GetSelected()
|
||||
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
|
||||
if selectedMenuItem != nil {
|
||||
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -37,9 +38,10 @@ func (self *OptionsMenuAction) Call() error {
|
||||
})
|
||||
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.Keybindings,
|
||||
Items: menuItems,
|
||||
HideCancel: true,
|
||||
Title: self.c.Tr.Keybindings,
|
||||
Items: menuItems,
|
||||
HideCancel: true,
|
||||
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
|
||||
}
|
||||
|
||||
self.setSubCommits(commits)
|
||||
self.c.Helpers().Refresh.RefreshAuthors(commits)
|
||||
|
||||
subCommitsContext := self.c.Contexts().SubCommits
|
||||
subCommitsContext.SetSelectedLineIdx(0)
|
||||
|
||||
@@ -141,7 +141,7 @@ func (self *TagsController) createResetMenu(tag *models.Tag) error {
|
||||
|
||||
func (self *TagsController) create() error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
return self.c.Helpers().Tags.CreateTagMenu("", func() { self.context().SetSelectedLineIdx(0) })
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt("", func() { self.context().SetSelectedLineIdx(0) })
|
||||
}
|
||||
|
||||
func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error {
|
||||
|
||||
@@ -132,6 +132,8 @@ type Gui struct {
|
||||
helpers *helpers.Helpers
|
||||
|
||||
integrationTest integrationTypes.IntegrationTest
|
||||
|
||||
afterLayoutFuncs chan func() error
|
||||
}
|
||||
|
||||
type StateAccessor struct {
|
||||
@@ -347,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),
|
||||
@@ -454,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()
|
||||
@@ -519,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
|
||||
}
|
||||
|
||||
@@ -168,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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,23 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
// if you run `lazygit --logs`
|
||||
// this will let you see these branches as prettified json
|
||||
// gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4]))
|
||||
return gui.helpers.Confirmation.ResizeCurrentPopupPanel()
|
||||
if err := gui.helpers.Confirmation.ResizeCurrentPopupPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outer:
|
||||
for {
|
||||
select {
|
||||
case f := <-gui.afterLayoutFuncs:
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
|
||||
|
||||
@@ -41,7 +41,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Contexts.Menu.SetMenuItems(opts.Items)
|
||||
gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment)
|
||||
gui.State.Contexts.Menu.SetSelectedLineIdx(0)
|
||||
|
||||
gui.Views.Menu.Title = opts.Title
|
||||
|
||||
@@ -19,7 +19,12 @@ func NewClient(
|
||||
helpers *helpers.Helpers,
|
||||
) *Client {
|
||||
sessionStateLoader := NewSessionStateLoader(c, helpers.Refs)
|
||||
handlerCreator := NewHandlerCreator(c, sessionStateLoader, helpers.Suggestions)
|
||||
handlerCreator := NewHandlerCreator(
|
||||
c,
|
||||
sessionStateLoader,
|
||||
helpers.Suggestions,
|
||||
helpers.MergeAndRebase,
|
||||
)
|
||||
keybindingCreator := NewKeybindingCreator(c)
|
||||
customCommands := c.UserConfig.CustomCommands
|
||||
|
||||
|
||||
@@ -17,27 +17,30 @@ import (
|
||||
|
||||
// takes a custom command and returns a function that will be called when the corresponding user-defined keybinding is pressed
|
||||
type HandlerCreator struct {
|
||||
c *helpers.HelperCommon
|
||||
sessionStateLoader *SessionStateLoader
|
||||
resolver *Resolver
|
||||
menuGenerator *MenuGenerator
|
||||
suggestionsHelper *helpers.SuggestionsHelper
|
||||
c *helpers.HelperCommon
|
||||
sessionStateLoader *SessionStateLoader
|
||||
resolver *Resolver
|
||||
menuGenerator *MenuGenerator
|
||||
suggestionsHelper *helpers.SuggestionsHelper
|
||||
mergeAndRebaseHelper *helpers.MergeAndRebaseHelper
|
||||
}
|
||||
|
||||
func NewHandlerCreator(
|
||||
c *helpers.HelperCommon,
|
||||
sessionStateLoader *SessionStateLoader,
|
||||
suggestionsHelper *helpers.SuggestionsHelper,
|
||||
mergeAndRebaseHelper *helpers.MergeAndRebaseHelper,
|
||||
) *HandlerCreator {
|
||||
resolver := NewResolver(c.Common)
|
||||
menuGenerator := NewMenuGenerator(c.Common)
|
||||
|
||||
return &HandlerCreator{
|
||||
c: c,
|
||||
sessionStateLoader: sessionStateLoader,
|
||||
resolver: resolver,
|
||||
menuGenerator: menuGenerator,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
c: c,
|
||||
sessionStateLoader: sessionStateLoader,
|
||||
resolver: resolver,
|
||||
menuGenerator: menuGenerator,
|
||||
suggestionsHelper: suggestionsHelper,
|
||||
mergeAndRebaseHelper: mergeAndRebaseHelper,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +275,16 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
||||
cmdObj.StreamOutput()
|
||||
}
|
||||
output, err := cmdObj.RunWithOutput()
|
||||
|
||||
if refreshErr := self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}); err != nil {
|
||||
self.c.Log.Error(refreshErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if customCommand.After.CheckForConflicts {
|
||||
return self.mergeAndRebaseHelper.CheckForConflicts(err)
|
||||
}
|
||||
|
||||
return self.c.Error(err)
|
||||
}
|
||||
|
||||
@@ -280,11 +292,9 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses
|
||||
if strings.TrimSpace(output) == "" {
|
||||
output = self.c.Tr.EmptyOutput
|
||||
}
|
||||
if err = self.c.Alert(cmdStr, output); err != nil {
|
||||
return self.c.Error(err)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{})
|
||||
return self.c.Alert(cmdStr, output)
|
||||
}
|
||||
return self.c.Refresh(types.RefreshOptions{})
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,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
|
||||
@@ -129,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 {
|
||||
@@ -212,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
|
||||
@@ -223,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
|
||||
|
||||
@@ -3,7 +3,9 @@ package gui
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type viewNameMapping struct {
|
||||
@@ -159,7 +161,10 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
gui.Views.CommitDescription.Visible = false
|
||||
gui.Views.CommitDescription.Title = gui.c.Tr.CommitDescriptionTitle
|
||||
gui.Views.CommitDescription.Subtitle = gui.Tr.CommitDescriptionSubTitle
|
||||
gui.Views.CommitDescription.Subtitle = utils.ResolvePlaceholderString(gui.Tr.CommitDescriptionSubTitle,
|
||||
map[string]string{
|
||||
"togglePanelKeyBinding": keybindings.Label(gui.UserConfig.Keybinding.Universal.TogglePanel),
|
||||
})
|
||||
gui.Views.CommitDescription.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitDescription.Editable = true
|
||||
gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor)
|
||||
|
||||
@@ -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: "检出提交",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -192,7 +192,7 @@ type TranslationSet struct {
|
||||
RecentRepos string
|
||||
MergeOptionsTitle string
|
||||
RebaseOptionsTitle string
|
||||
CommitMessageTitle string
|
||||
CommitSummaryTitle string
|
||||
CommitDescriptionTitle string
|
||||
CommitDescriptionSubTitle string
|
||||
LocalBranchesTitle string
|
||||
@@ -355,7 +355,7 @@ type TranslationSet struct {
|
||||
PushTagTitle string
|
||||
PushTag string
|
||||
CreateTag string
|
||||
CreateTagTitle string
|
||||
CreatingTag string
|
||||
FetchRemote string
|
||||
FetchingRemoteStatus string
|
||||
CheckoutCommit string
|
||||
@@ -884,9 +884,9 @@ func EnglishTranslationSet() TranslationSet {
|
||||
RecentRepos: "Recent repositories",
|
||||
MergeOptionsTitle: "Merge options",
|
||||
RebaseOptionsTitle: "Rebase options",
|
||||
CommitMessageTitle: "Commit summary",
|
||||
CommitSummaryTitle: "Commit summary",
|
||||
CommitDescriptionTitle: "Commit description",
|
||||
CommitDescriptionSubTitle: "Press tab to toggle focus",
|
||||
CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus",
|
||||
LocalBranchesTitle: "Local branches",
|
||||
SearchTitle: "Search",
|
||||
TagsTitle: "Tags",
|
||||
@@ -1039,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",
|
||||
@@ -1049,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",
|
||||
|
||||
@@ -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: "コミットをチェックアウト",
|
||||
|
||||
@@ -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: "커밋을 체크아웃",
|
||||
|
||||
@@ -216,7 +216,7 @@ func RussianTranslationSet() TranslationSet {
|
||||
RecentRepos: "Последние репозитории",
|
||||
MergeOptionsTitle: "Параметры слияния",
|
||||
RebaseOptionsTitle: "Параметры перебазирования",
|
||||
CommitMessageTitle: "Сводка коммита",
|
||||
CommitSummaryTitle: "Сводка коммита",
|
||||
CommitDescriptionTitle: "Описание коммита",
|
||||
CommitDescriptionSubTitle: "Нажмите вкладку, чтобы переключить фокус",
|
||||
LocalBranchesTitle: "Локальные Ветки",
|
||||
@@ -371,8 +371,8 @@ func RussianTranslationSet() TranslationSet {
|
||||
EditRemote: "Редактировать удалённый репозитории",
|
||||
TagCommit: "Пометить коммит тегом",
|
||||
TagMenuTitle: "Создать тег",
|
||||
TagNameTitle: "Название тега:",
|
||||
TagMessageTitle: "Сообщения тега:",
|
||||
TagNameTitle: "Название тега",
|
||||
TagMessageTitle: "Сообщения тега",
|
||||
AnnotatedTag: "Аннотированный тег",
|
||||
LightweightTag: "Легковесный тег",
|
||||
DeleteTag: "Удалить тег",
|
||||
@@ -381,7 +381,6 @@ func RussianTranslationSet() TranslationSet {
|
||||
PushTagTitle: "Удалённый репозитории для отправки тега '{{.tagName}}' в:",
|
||||
PushTag: "Отправить тег",
|
||||
CreateTag: "Создать тег",
|
||||
CreateTagTitle: "Название тега:",
|
||||
FetchRemote: "Получение изменения из удалённого репозитория",
|
||||
FetchingRemoteStatus: "Получение статуса удалённого репозитория",
|
||||
CheckoutCommit: "Переключить коммит",
|
||||
|
||||
@@ -247,7 +247,7 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
RecentRepos: "最近的版本庫",
|
||||
MergeOptionsTitle: "合併選項",
|
||||
RebaseOptionsTitle: "變基選項",
|
||||
CommitMessageTitle: "提交摘要",
|
||||
CommitSummaryTitle: "提交摘要",
|
||||
CommitDescriptionTitle: "提交描述",
|
||||
CommitDescriptionSubTitle: "按 tab 切換焦點",
|
||||
LocalBranchesTitle: "本地分支",
|
||||
@@ -398,8 +398,8 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
EditRemote: "編輯遠端",
|
||||
TagCommit: "打標籤到提交",
|
||||
TagMenuTitle: "建立標籤",
|
||||
TagNameTitle: "標籤名稱:",
|
||||
TagMessageTitle: "標籤訊息:",
|
||||
TagNameTitle: "標籤名稱",
|
||||
TagMessageTitle: "標籤訊息",
|
||||
AnnotatedTag: "附註標籤",
|
||||
LightweightTag: "輕量標籤",
|
||||
DeleteTag: "刪除標籤",
|
||||
@@ -408,7 +408,6 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
PushTagTitle: "推送標籤 '{{.tagName}}' 至遠端:",
|
||||
PushTag: "推送標籤",
|
||||
CreateTag: "建立標籤",
|
||||
CreateTagTitle: "標籤名稱:",
|
||||
FetchRemote: "擷取遠端",
|
||||
FetchingRemoteStatus: "正在擷取遠端",
|
||||
CheckoutCommit: "檢出提交",
|
||||
|
||||
@@ -28,7 +28,10 @@ func RunTUI() {
|
||||
app := newApp(testDir)
|
||||
app.loadTests()
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, false, false, false, gui.RuneReplacements)
|
||||
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
||||
OutputMode: gocui.OutputTrue,
|
||||
RuneReplacements: gui.RuneReplacements,
|
||||
})
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
@@ -23,3 +23,9 @@ func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDr
|
||||
self.t.press(self.t.keys.Universal.Confirm)
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
|
||||
self.getViewDriver().Title(expected)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import (
|
||||
// to get the test's name via it's file's path.
|
||||
const unitTestDescription = "test test"
|
||||
|
||||
const (
|
||||
defaultWidth = 100
|
||||
defaultHeight = 100
|
||||
)
|
||||
|
||||
type IntegrationTest struct {
|
||||
name string
|
||||
description string
|
||||
@@ -32,6 +37,8 @@ type IntegrationTest struct {
|
||||
keys config.KeybindingConfig,
|
||||
)
|
||||
gitVersion GitVersionRestriction
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
|
||||
@@ -52,6 +59,11 @@ type NewIntegrationTestArgs struct {
|
||||
Skip bool
|
||||
// to run a test only on certain git versions
|
||||
GitVersion GitVersionRestriction
|
||||
// width and height when running in headless mode, for testing
|
||||
// the UI in different sizes.
|
||||
// If these are set, the test must be run in headless mode
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type GitVersionRestriction struct {
|
||||
@@ -120,6 +132,8 @@ func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
setupConfig: args.SetupConfig,
|
||||
run: args.Run,
|
||||
gitVersion: args.GitVersion,
|
||||
width: args.Width,
|
||||
height: args.Height,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +186,18 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) HeadlessDimensions() (int, int) {
|
||||
if self.width == 0 && self.height == 0 {
|
||||
return defaultWidth, defaultHeight
|
||||
}
|
||||
|
||||
return self.width, self.height
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) RequiresHeadless() bool {
|
||||
return self.width != 0 && self.height != 0
|
||||
}
|
||||
|
||||
func testNameFromCurrentFilePath() string {
|
||||
path := utils.FilePath(3)
|
||||
return TestNameFromFilePath(path)
|
||||
|
||||
@@ -82,6 +82,20 @@ func (self *ViewDriver) TopLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
return self.assertLines(0, matchers...)
|
||||
}
|
||||
|
||||
// Asserts on the visible lines of the view.
|
||||
// Note, this assumes that the view's viewport is filled with lines
|
||||
func (self *ViewDriver) VisibleLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
self.validateMatchersPassed(matchers)
|
||||
self.validateVisibleLineCount(matchers)
|
||||
|
||||
// Get the origin of the view and offset that.
|
||||
// Note that we don't do any retrying here so if we want to bring back retry logic
|
||||
// we'll need to update this.
|
||||
originY := self.getView().OriginY()
|
||||
|
||||
return self.assertLines(originY, matchers...)
|
||||
}
|
||||
|
||||
// asserts that somewhere in the view there are consequetive lines matching the given matchers.
|
||||
func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver {
|
||||
self.validateMatchersPassed(matchers)
|
||||
@@ -212,6 +226,16 @@ func (self *ViewDriver) validateEnoughLines(matchers []*TextMatcher) {
|
||||
})
|
||||
}
|
||||
|
||||
// assumes the view's viewport is filled with lines
|
||||
func (self *ViewDriver) validateVisibleLineCount(matchers []*TextMatcher) {
|
||||
view := self.getView()
|
||||
|
||||
self.t.assertWithRetries(func() (bool, string) {
|
||||
count := view.InnerHeight() + 1
|
||||
return count == len(matchers), fmt.Sprintf("unexpected number of visible lines in view '%s'. Expected exactly %d, got %d", view.Name(), len(matchers), count)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewDriver {
|
||||
view := self.getView()
|
||||
|
||||
@@ -515,6 +539,7 @@ func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
|
||||
self.Press(self.t.keys.Universal.StartSearch).
|
||||
Tap(func() {
|
||||
self.t.ExpectSearch().
|
||||
Clear().
|
||||
Type(text).
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -26,13 +26,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
SelectNextItem().
|
||||
Press(keys.Branches.CreateTag)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -23,13 +23,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Commits.CreateTag)
|
||||
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
|
||||
@@ -5,29 +5,58 @@ import (
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// Originally we only suggested authors present in the current branch, but now
|
||||
// we include authors from other branches whose commits you've looked at in the
|
||||
// lazygit session.
|
||||
|
||||
var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Set author on a commit",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("original")
|
||||
|
||||
shell.SetConfig("user.email", "Bill@example.com")
|
||||
shell.SetConfig("user.name", "Bill Smith")
|
||||
|
||||
shell.EmptyCommit("one")
|
||||
|
||||
shell.NewBranch("other")
|
||||
|
||||
shell.SetConfig("user.email", "John@example.com")
|
||||
shell.SetConfig("user.name", "John Smith")
|
||||
|
||||
shell.EmptyCommit("two")
|
||||
|
||||
shell.Checkout("original")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("BS").Contains("one").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("original").IsSelected(),
|
||||
Contains("other"),
|
||||
).
|
||||
NavigateToLine(Contains("other")).
|
||||
PressEnter()
|
||||
|
||||
// ensuring we get these commit authors as suggestions
|
||||
t.Views().SubCommits().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("JS").Contains("two").IsSelected(),
|
||||
Contains("BS").Contains("one"),
|
||||
).
|
||||
)
|
||||
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Press(keys.Commits.ResetCommitAuthor).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
@@ -38,14 +67,13 @@ var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Contains("Set author")).
|
||||
SuggestionLines(
|
||||
Contains("John Smith"),
|
||||
Contains("Bill Smith"),
|
||||
Contains("John Smith"),
|
||||
).
|
||||
ConfirmSuggestion(Contains("John Smith"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("JS").Contains("two").IsSelected(),
|
||||
Contains("BS").Contains("one"),
|
||||
Contains("JS").Contains("one").IsSelected(),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
40
pkg/integration/tests/custom_commands/check_for_conflicts.go
Normal file
40
pkg/integration/tests/custom_commands/check_for_conflicts.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
|
||||
)
|
||||
|
||||
var CheckForConflicts = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Run a command and check for conflicts after",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shared.MergeConflictsSetup(shell)
|
||||
},
|
||||
SetupConfig: func(cfg *config.AppConfig) {
|
||||
cfg.UserConfig.CustomCommands = []config.CustomCommand{
|
||||
{
|
||||
Key: "m",
|
||||
Context: "localBranches",
|
||||
Command: "git merge {{ .SelectedLocalBranch.Name | quote }}",
|
||||
After: config.CustomCommandAfterHook{
|
||||
CheckForConflicts: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Branches().
|
||||
Focus().
|
||||
TopLines(
|
||||
Contains("first-change-branch"),
|
||||
Contains("second-change-branch"),
|
||||
).
|
||||
NavigateToLine(Contains("second-change-branch")).
|
||||
Press("m")
|
||||
|
||||
t.Common().AcknowledgeConflicts()
|
||||
},
|
||||
})
|
||||
35
pkg/integration/tests/filter_and_search/filter_fuzzy.go
Normal file
35
pkg/integration/tests/filter_and_search/filter_fuzzy.go
Normal file
@@ -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`),
|
||||
)
|
||||
},
|
||||
})
|
||||
37
pkg/integration/tests/tag/create_while_committing.go
Normal file
37
pkg/integration/tests/tag/create_while_committing.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var CreateWhileCommitting = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Draft a commit message, escape out, and make a tag. Verify the draft message doesn't appear in the tag create prompt",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.EmptyCommit("initial commit")
|
||||
shell.CreateFileAndAdd("file.txt", "file contents")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
Press(keys.Files.CommitChanges).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Commit summary")).
|
||||
Type("draft message").
|
||||
Cancel()
|
||||
})
|
||||
|
||||
t.Views().Tags().
|
||||
Focus().
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
InitialText(Equals(""))
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -19,19 +19,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Annotated")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag message:")).
|
||||
SwitchToDescription().
|
||||
Title(Equals("Tag description")).
|
||||
Type("message").
|
||||
SwitchToSummary().
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
@@ -44,6 +38,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Content(Equals("Are you sure you want to delete tag 'new-tag'?")).
|
||||
Confirm()
|
||||
}).
|
||||
IsEmpty()
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
// confirm content is cleared on next tag create
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
InitialText(Equals(""))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -19,13 +19,8 @@ var CrudLightweight = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
IsEmpty().
|
||||
Press(keys.Universal.New).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Create tag")).
|
||||
Select(Contains("Lightweight")).
|
||||
Confirm()
|
||||
|
||||
t.ExpectPopup().Prompt().
|
||||
Title(Equals("Tag name:")).
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Tag name")).
|
||||
Type("new-tag").
|
||||
Confirm()
|
||||
}).
|
||||
|
||||
@@ -75,6 +75,7 @@ var tests = []*components.IntegrationTest{
|
||||
conflicts.UndoChooseHunk,
|
||||
custom_commands.BasicCmdAtRuntime,
|
||||
custom_commands.BasicCmdFromConfig,
|
||||
custom_commands.CheckForConflicts,
|
||||
custom_commands.ComplexCmdAtRuntime,
|
||||
custom_commands.FormPrompts,
|
||||
custom_commands.MenuFromCommand,
|
||||
@@ -97,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,
|
||||
@@ -205,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
pkg/integration/tests/ui/accordion.go
Normal file
61
pkg/integration/tests/ui/accordion.go
Normal 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
pkg/integration/tests/ui/empty_menu.go
Normal file
31
pkg/integration/tests/ui/empty_menu.go
Normal 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()
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
29
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
29
vendor/github.com/jesseduffield/gocui/gui.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
4
vendor/github.com/jesseduffield/gocui/tcell_driver.go
generated
vendored
4
vendor/github.com/jesseduffield/gocui/tcell_driver.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
48
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
48
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20230710004407-9bbfd873713b
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
|
||||
Reference in New Issue
Block a user