Compare commits

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

I've had to update other uses of that prompt to allow the summary and description labels to
be passed in
2023-07-22 14:36:35 +10:00
Jesse Duffield b284970bac Use fuzzy search when filtering a view (#2808) 2023-07-22 13:17:46 +10:00
Jesse Duffield b46623ebef Use fuzzy search when filtering a view
This adds fuzzy filtering instead of exact match filtering, which is more forgiving of typos
and allows more efficiency.
2023-07-22 13:14:29 +10:00
Jesse Duffield 084c0a19bc Include more commit authors in author suggestions (#2807) 2023-07-22 11:00:38 +10:00
Jesse Duffield 3cee37388c Keep track of authors across local commits and branch commits for suggestions
Previously, we would only show the authors based on local commits, but sometimes you want to set a commit author
to that of a commit on another branch. Now, so long as you've viewed the branch's commits, the author will appear
as a suggestion.
2023-07-22 10:47:04 +10:00
Andrew Savinykh a7969aef2c Fix rendering to main view on windows 2023-07-22 09:14:05 +10:00
Jesse Duffield 39c900c7e7 Fix goreleaser 2023-07-21 09:03:47 +10:00
Jesse Duffield 6e247c1583 Only apply right-alignment on first column of keybindings menu (#2801) 2023-07-20 21:27:01 +10:00
Jesse Duffield 87bf1dbc7f Only apply right-alignment on first column of keybindings menu
Previously we applied a right-align on the first column of _all_ menus, even though we really
only intended for it to be on the first column of the keybindings menu (that you get from pressing
'?')
2023-07-20 21:23:46 +10:00
Jesse Duffield 1f920ae6ba Fix crash on empty menu (#2799) 2023-07-20 21:17:14 +10:00
Jesse Duffield 932e01b41a Add test for crashing on empty menu 2023-07-20 21:08:56 +10:00
Jesse Duffield 373f24c80f Fix crash on empty menu
When a menu is empty (e.g. due to filtering) we shouldn't crash on focus or selection
2023-07-20 21:05:52 +10:00
Jesse Duffield a548b289ef Add missing label to label checker (#2798) 2023-07-20 17:33:10 +10:00
Jesse Duffield b168fc8cdd Add missing label to label checker 2023-07-20 17:31:40 +10:00
Jesse Duffield 94845dcf98 Update release notes config and add CI check (#2797) 2023-07-20 17:19:49 +10:00
Jesse Duffield 1af6dff64e Update release notes config and add CI check 2023-07-20 17:15:10 +10:00
Jesse Duffield 72de4f436e Add release config for generating release notes (#2793) 2023-07-19 23:38:04 +10:00
54 changed files with 526 additions and 256 deletions
+2 -2
View File
@@ -8,10 +8,10 @@ changelog:
- feature - feature
- title: Enhancements 🔥 - title: Enhancements 🔥
labels: labels:
- improvement - enhancement
- title: Fixes 🔧 - title: Fixes 🔧
labels: labels:
- bugfix - bug
- title: Maintenance ⚙️ - title: Maintenance ⚙️
labels: labels:
- maintenance - maintenance
+4
View File
@@ -19,6 +19,10 @@ jobs:
go-version: 1.18.x go-version: 1.18.x
- name: Run goreleaser - name: Run goreleaser
uses: goreleaser/goreleaser-action@v1 uses: goreleaser/goreleaser-action@v1
with:
distribution: goreleaser
version: v1.17.2
args: release --clean
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
homebrew: homebrew:
+8
View File
@@ -204,3 +204,11 @@ jobs:
- name: errors - name: errors
run: golangci-lint run run: golangci-lint run
if: ${{ failure() }} 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"
+1 -1
View File
@@ -12,7 +12,7 @@ builds:
- amd64 - amd64
- arm - arm
- arm64 - arm64
- 386 - '386'
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`. # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags: ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
+16 -15
View File
@@ -2,18 +2,17 @@
<img width="536" src="https://user-images.githubusercontent.com/8456633/174470852-339b5011-5800-4bb9-a628-ff230aa8cd4e.png"> <img width="536" src="https://user-images.githubusercontent.com/8456633/174470852-339b5011-5800-4bb9-a628-ff230aa8cd4e.png">
</p> </p>
<div align="center">
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) A simple terminal UI for git commands
[![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) <br/>
[![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com)
[![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit)
[![GitHub Releases](https://img.shields.io/github/downloads/jesseduffield/lazygit/total)](https://github.com/jesseduffield/lazygit/releases)
[![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)](https://github.com/jesseduffield/lazygit/releases/latest)
[![homebrew](https://img.shields.io/homebrew/v/lazygit)](https://github.com/Homebrew/homebrew-core/blob/master/Formula/lazygit.rb)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library. [![GitHub Releases](https://img.shields.io/github/downloads/jesseduffield/lazygit/total)](https://github.com/jesseduffield/lazygit/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)](https://github.com/jesseduffield/lazygit/releases/latest) [![homebrew](https://img.shields.io/homebrew/v/lazygit)](https://github.com/Homebrew/homebrew-core/blob/master/Formula/lazygit.rb)
![Gif](../assets/staging.gif) ![Gif](../assets/staging.gif)
</div>
## Sponsors ## Sponsors
@@ -278,16 +277,18 @@ See the [docs](docs/Custom_Command_Keybindings.md)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY) - [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs) - [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Cool features ## Cool features
- Adding files easily - Add files easily
- Resolving merge conflicts - Resolve merge conflicts
- Easily check out recent branches - Fuzzy searching
- Scroll through logs/diffs of branches/commits/stash - Git bisect
- Quick pushing/pulling - Amend old commits
- Squash down and reword commits - Slice, dice, squash, and rearrange commits
- Submodules support
- diff branches easily
- Cherry-pick commits like it's copy+paste
### Resolving merge conflicts ### Resolving merge conflicts
+1 -1
View File
@@ -18,7 +18,7 @@ require (
github.com/integrii/flaggy v1.4.0 github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
+2 -2
View File
@@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk= github.com/jesseduffield/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 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/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f h1:w/pxI34XepTAx4HwxUu8ipimbVRgSTS+7ahmgFQwH80= github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144 h1:gwy5JzP6+PhcPFG1obkUSLGcTkUY88sLKlCPOFjwtak=
github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s= 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 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= 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= github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
+1 -1
View File
@@ -101,7 +101,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
"reflogCommits": tr.ReflogCommitsTitle, "reflogCommits": tr.ReflogCommitsTitle,
"tags": tr.TagsTitle, "tags": tr.TagsTitle,
"commitFiles": tr.CommitFilesTitle, "commitFiles": tr.CommitFilesTitle,
"commitMessage": tr.CommitMessageTitle, "commitMessage": tr.CommitSummaryTitle,
"commitDescription": tr.CommitDescriptionTitle, "commitDescription": tr.CommitDescriptionTitle,
"commits": tr.CommitsTitle, "commits": tr.CommitsTitle,
"confirmation": tr.ConfirmationTitle, "confirmation": tr.ConfirmationTitle,
+7 -8
View File
@@ -50,13 +50,13 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
Run() Run()
} }
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj { func (self *CommitCommands) CommitCmdObj(summary string, description string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(message) messageArgs := self.commitMessageArgs(summary, description)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
cmdArgs := NewGitCmd("commit"). cmdArgs := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify"). ArgIf(skipHookPrefix != "" && strings.HasPrefix(summary, skipHookPrefix), "--no-verify").
ArgIf(self.signoffFlag() != "", self.signoffFlag()). ArgIf(self.signoffFlag() != "", self.signoffFlag()).
Arg(messageArgs...). Arg(messageArgs...).
ToArgv() ToArgv()
@@ -69,8 +69,8 @@ func (self *CommitCommands) RewordLastCommitInEditorCmdObj() oscommands.ICmdObj
} }
// RewordLastCommit rewords the topmost commit with the given message // RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error { func (self *CommitCommands) RewordLastCommit(summary string, description string) error {
messageArgs := self.commitMessageArgs(message) messageArgs := self.commitMessageArgs(summary, description)
cmdArgs := NewGitCmd("commit"). cmdArgs := NewGitCmd("commit").
Arg("--allow-empty", "--amend", "--only"). Arg("--allow-empty", "--amend", "--only").
@@ -80,9 +80,8 @@ func (self *CommitCommands) RewordLastCommit(message string) error {
return self.cmd.New(cmdArgs).Run() return self.cmd.New(cmdArgs).Run()
} }
func (self *CommitCommands) commitMessageArgs(message string) []string { func (self *CommitCommands) commitMessageArgs(summary string, description string) []string {
msg, description, _ := strings.Cut(message, "\n") args := []string{"-m", summary}
args := []string{"-m", msg}
if description != "" { if description != "" {
args = append(args, "-m", description) args = append(args, "-m", description)
+17 -12
View File
@@ -10,20 +10,23 @@ import (
func TestCommitRewordCommit(t *testing.T) { func TestCommitRewordCommit(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
runner *oscommands.FakeCmdObjRunner runner *oscommands.FakeCmdObjRunner
input string summary string
description string
} }
scenarios := []scenario{ scenarios := []scenario{
{ {
"Single line reword", "Single line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil), oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
"test", "test",
"",
}, },
{ {
"Multi line reword", "Multi line reword",
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil), 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 { for _, s := range scenarios {
@@ -31,7 +34,7 @@ func TestCommitRewordCommit(t *testing.T) {
t.Run(s.testName, func(t *testing.T) { t.Run(s.testName, func(t *testing.T) {
instance := buildCommitCommands(commonDeps{runner: s.runner}) 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() s.runner.CheckForMissingCalls()
}) })
} }
@@ -50,7 +53,8 @@ func TestCommitResetToCommit(t *testing.T) {
func TestCommitCommitCmdObj(t *testing.T) { func TestCommitCommitCmdObj(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string
message string summary string
description string
configSignoff bool configSignoff bool
configSkipHookPrefix string configSkipHookPrefix string
expectedArgs []string expectedArgs []string
@@ -59,35 +63,36 @@ func TestCommitCommitCmdObj(t *testing.T) {
scenarios := []scenario{ scenarios := []scenario{
{ {
testName: "Commit", testName: "Commit",
message: "test", summary: "test",
configSignoff: false, configSignoff: false,
configSkipHookPrefix: "", configSkipHookPrefix: "",
expectedArgs: []string{"commit", "-m", "test"}, expectedArgs: []string{"commit", "-m", "test"},
}, },
{ {
testName: "Commit with --no-verify flag", testName: "Commit with --no-verify flag",
message: "WIP: test", summary: "WIP: test",
configSignoff: false, configSignoff: false,
configSkipHookPrefix: "WIP", configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"}, expectedArgs: []string{"commit", "--no-verify", "-m", "WIP: test"},
}, },
{ {
testName: "Commit with multiline message", testName: "Commit with multiline message",
message: "line1\nline2", summary: "line1",
description: "line2",
configSignoff: false, configSignoff: false,
configSkipHookPrefix: "", configSkipHookPrefix: "",
expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"}, expectedArgs: []string{"commit", "-m", "line1", "-m", "line2"},
}, },
{ {
testName: "Commit with signoff", testName: "Commit with signoff",
message: "test", summary: "test",
configSignoff: true, configSignoff: true,
configSkipHookPrefix: "", configSkipHookPrefix: "",
expectedArgs: []string{"commit", "--signoff", "-m", "test"}, expectedArgs: []string{"commit", "--signoff", "-m", "test"},
}, },
{ {
testName: "Commit with signoff and no-verify", testName: "Commit with signoff and no-verify",
message: "WIP: test", summary: "WIP: test",
configSignoff: true, configSignoff: true,
configSkipHookPrefix: "WIP", configSkipHookPrefix: "WIP",
expectedArgs: []string{"commit", "--no-verify", "--signoff", "-m", "WIP: test"}, 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) runner := oscommands.NewFakeRunner(t).ExpectGitArgs(s.expectedArgs, "", nil)
instance := buildCommitCommands(commonDeps{userConfig: userConfig, runner: runner}) 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() runner.CheckForMissingCalls()
}) })
} }
+1 -1
View File
@@ -302,7 +302,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
head_message, _ := self.commit.GetHeadCommitMessage() head_message, _ := self.commit.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message) 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 return err
} }
+3 -3
View File
@@ -35,10 +35,10 @@ func NewRebaseCommands(
} }
} }
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error { func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, summary string, description string) error {
if models.IsHeadCommit(commits, index) { if models.IsHeadCommit(commits, index) {
// we've selected the top commit so no rebase is required // 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) 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 // 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 { if err != nil {
return err return err
} }
+13
View File
@@ -0,0 +1,13 @@
package models
import "fmt"
// A commit author
type Author struct {
Name string
Email string
}
func (self *Author) Combined() string {
return fmt.Sprintf("%s <%s>", self.Name, self.Email)
}
+12 -5
View File
@@ -31,7 +31,7 @@ type CommitMessageViewModel struct {
// the full preserved message (combined summary and description) // the full preserved message (combined summary and description)
preservedMessage string preservedMessage string
// invoked when pressing enter in the commit message panel // 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 // The message typed in before cycling through history
// We store this separately to 'preservedMessage' because 'preservedMessage' // We store this separately to 'preservedMessage' because 'preservedMessage'
@@ -88,15 +88,22 @@ func (self *CommitMessageContext) SetHistoryMessage(message string) {
self.viewModel.historyMessage = message self.viewModel.historyMessage = message
} }
func (self *CommitMessageContext) OnConfirm(message string) error { func (self *CommitMessageContext) OnConfirm(summary string, description string) error {
return self.viewModel.onConfirm(message) 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.selectedindex = index
self.viewModel.preserveMessage = preserveMessage self.viewModel.preserveMessage = preserveMessage
self.viewModel.onConfirm = onConfirm self.viewModel.onConfirm = onConfirm
self.GetView().Title = title self.GetView().Title = summaryTitle
self.c.Views().CommitDescription.Title = descriptionTitle
} }
func (self *CommitMessageContext) RenderCommitLength() { func (self *CommitMessageContext) RenderCommitLength() {
+27 -12
View File
@@ -1,7 +1,11 @@
package context package context
import ( import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sahilm/fuzzy"
"github.com/samber/lo"
"github.com/sasha-s/go-deadlock" "github.com/sasha-s/go-deadlock"
) )
@@ -53,6 +57,21 @@ func (self *FilteredList[T]) UnfilteredLen() int {
return len(self.getList()) 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() { func (self *FilteredList[T]) applyFilter() {
self.mutex.Lock() self.mutex.Lock()
defer self.mutex.Unlock() defer self.mutex.Unlock()
@@ -60,20 +79,16 @@ func (self *FilteredList[T]) applyFilter() {
if self.filter == "" { if self.filter == "" {
self.filteredIndices = nil self.filteredIndices = nil
} else { } else {
self.filteredIndices = []int{} source := &fuzzySource[T]{
for i, item := range self.getList() { list: self.getList(),
for _, field := range self.getFilterFields(item) { getFilterFields: self.getFilterFields,
if self.match(field, self.filter) {
self.filteredIndices = append(self.filteredIndices, i)
break
}
}
} }
}
}
func (self *FilteredList[T]) match(haystack string, needle string) bool { matches := fuzzy.FindFrom(self.filter, source)
return utils.CaseAwareContains(haystack, needle) self.filteredIndices = lo.Map(matches, func(match fuzzy.Match, _ int) int {
return match.Index
})
}
} }
func (self *FilteredList[T]) UnfilteredIndex(index int) int { func (self *FilteredList[T]) UnfilteredIndex(index int) int {
+6 -2
View File
@@ -14,7 +14,7 @@ type ListContextTrait struct {
list types.IList list types.IList
getDisplayStrings func(startIdx int, length int) [][]string getDisplayStrings func(startIdx int, length int) [][]string
// Alignment for each column. If nil, the default is left alignment // 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 // 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 // to its parents, because it's ambiguous otherwise. For these, we need to refresh the viewport
// so that we show the highlighted path. // so that we show the highlighted path.
@@ -82,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 // 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 { func (self *ListContextTrait) HandleRender() error {
self.list.RefreshSelectedIdx() self.list.RefreshSelectedIdx()
var columnAlignments []utils.Alignment
if self.getColumnAlignments != nil {
columnAlignments = self.getColumnAlignments()
}
content := utils.RenderDisplayStrings( content := utils.RenderDisplayStrings(
self.getDisplayStrings(0, self.list.Len()), self.getDisplayStrings(0, self.list.Len()),
self.columnAlignments, columnAlignments,
) )
self.GetViewTrait().SetContent(content) self.GetViewTrait().SetContent(content)
self.c.Render() self.c.Render()
+13 -7
View File
@@ -35,10 +35,10 @@ func NewMenuContext(
Focusable: true, Focusable: true,
HasUncontrolledBounds: true, HasUncontrolledBounds: true,
})), })),
getDisplayStrings: viewModel.GetDisplayStrings, getDisplayStrings: viewModel.GetDisplayStrings,
list: viewModel, list: viewModel,
c: c, c: c,
columnAlignments: []utils.Alignment{utils.AlignRight, utils.AlignLeft}, getColumnAlignments: func() []utils.Alignment { return viewModel.columnAlignment },
}, },
} }
} }
@@ -54,8 +54,9 @@ func (self *MenuContext) GetSelectedItemId() string {
} }
type MenuViewModel struct { type MenuViewModel struct {
c *ContextCommon c *ContextCommon
menuItems []*types.MenuItem menuItems []*types.MenuItem
columnAlignment []utils.Alignment
*FilteredListViewModel[*types.MenuItem] *FilteredListViewModel[*types.MenuItem]
} }
@@ -73,8 +74,9 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
return self return self
} }
func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) { func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment []utils.Alignment) {
self.menuItems = items self.menuItems = items
self.columnAlignment = columnAlignment
} }
// TODO: move into presentation package // TODO: move into presentation package
@@ -135,6 +137,10 @@ func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
return err return err
} }
if selectedItem == nil {
return nil
}
if err := selectedItem.OnPress(); err != nil { if err := selectedItem.OnPress(); err != nil {
return err return err
} }
+1 -1
View File
@@ -74,7 +74,7 @@ func (gui *Gui) resetHelpersAndControllers() {
Suggestions: suggestionsHelper, Suggestions: suggestionsHelper,
Files: helpers.NewFilesHelper(helperCommon), Files: helpers.NewFilesHelper(helperCommon),
WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper), WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper),
Tags: helpers.NewTagsHelper(helperCommon), Tags: helpers.NewTagsHelper(helperCommon, commitsHelper),
GPG: helpers.NewGpgHelper(helperCommon), GPG: helpers.NewGpgHelper(helperCommon),
MergeAndRebase: rebaseHelper, MergeAndRebase: rebaseHelper,
MergeConflicts: mergeConflictsHelper, MergeConflicts: mergeConflictsHelper,
+1 -1
View File
@@ -395,7 +395,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
} }
func (self *BranchesController) createTag(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 { func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error {
+27 -17
View File
@@ -63,33 +63,44 @@ func (self *CommitsHelper) JoinCommitMessageAndDescription() string {
} }
func (self *CommitsHelper) UpdateCommitPanelView(message string) { func (self *CommitsHelper) UpdateCommitPanelView(message string) {
// first try the passed in message, if not fallback to context -> view in that order
if message != "" { if message != "" {
self.SetMessageAndDescriptionInView(message) self.SetMessageAndDescriptionInView(message)
return return
} }
message = self.c.Contexts().CommitMessage.GetPreservedMessage()
if message != "" { if self.c.Contexts().CommitMessage.GetPreserveMessage() {
self.SetMessageAndDescriptionInView(message) preservedMessage := self.c.Contexts().CommitMessage.GetPreservedMessage()
} else { self.SetMessageAndDescriptionInView(preservedMessage)
self.SetMessageAndDescriptionInView(self.getCommitSummary()) return
} }
self.SetMessageAndDescriptionInView("")
} }
type OpenCommitMessagePanelOpts struct { type OpenCommitMessagePanelOpts struct {
CommitIndex int CommitIndex int
Title string SummaryTitle string
PreserveMessage bool DescriptionTitle string
OnConfirm func(string) error PreserveMessage bool
InitialMessage string OnConfirm func(summary string, description string) error
InitialMessage string
} }
func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error { 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( self.c.Contexts().CommitMessage.SetPanelState(
opts.CommitIndex, opts.CommitIndex,
opts.Title, opts.SummaryTitle,
opts.DescriptionTitle,
opts.PreserveMessage, opts.PreserveMessage,
opts.OnConfirm, onConfirm,
) )
self.UpdateCommitPanelView(opts.InitialMessage) self.UpdateCommitPanelView(opts.InitialMessage)
@@ -102,17 +113,16 @@ func (self *CommitsHelper) OnCommitSuccess() {
if self.c.Contexts().CommitMessage.GetPreserveMessage() { if self.c.Contexts().CommitMessage.GetPreserveMessage() {
self.c.Contexts().CommitMessage.SetPreservedMessage("") self.c.Contexts().CommitMessage.SetPreservedMessage("")
} }
self.SetMessageAndDescriptionInView("")
} }
func (self *CommitsHelper) HandleCommitConfirm() error { 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) 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 { if err != nil {
return err return err
} }
@@ -302,7 +302,12 @@ func (self *ConfirmationHelper) resizeMenu() {
_, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0) _, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0)
tooltipTop := menuBottom + 1 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) _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
} }
@@ -266,6 +266,7 @@ func (self *RefreshHelper) refreshCommitsWithLimit() error {
return err return err
} }
self.c.Model().Commits = commits self.c.Model().Commits = commits
self.RefreshAuthors(commits)
self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState() self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState()
return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits)
@@ -287,10 +288,26 @@ func (self *RefreshHelper) refreshSubCommitsWithLimit() error {
return err return err
} }
self.c.Model().SubCommits = commits self.c.Model().SubCommits = commits
self.RefreshAuthors(commits)
return self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) 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 { func (self *RefreshHelper) refreshCommitFilesContext() error {
ref := self.c.Contexts().CommitFiles.GetRef() ref := self.c.Contexts().CommitFiles.GetRef()
to := ref.RefName() to := ref.RefName()
@@ -176,9 +176,11 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su
} }
func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion { func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion {
authors := lo.Uniq(slices.Map(self.c.Model().Commits, func(commit *models.Commit) string { authors := lo.Map(lo.Values(self.c.Model().Authors), func(author *models.Author, _ int) string {
return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail) return author.Combined()
})) })
slices.Sort(authors)
return FuzzySearchFunc(authors) return FuzzySearchFunc(authors)
} }
+38 -61
View File
@@ -1,77 +1,54 @@
package helpers package helpers
import ( import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types" "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 { type TagsHelper struct {
c *HelperCommon c *HelperCommon
commitsHelper *CommitsHelper
} }
func NewTagsHelper(c *HelperCommon) *TagsHelper { func NewTagsHelper(c *HelperCommon, commitsHelper *CommitsHelper) *TagsHelper {
return &TagsHelper{ return &TagsHelper{
c: c, c: c,
commitsHelper: commitsHelper,
} }
} }
func (self *TagsHelper) CreateTagMenu(ref string, onCreate func()) error { func (self *TagsHelper) OpenCreateTagPrompt(ref string, onCreate func()) error {
return self.c.Menu(types.CreateMenuOptions{ onConfirm := func(tagName string, description string) error {
Title: self.c.Tr.TagMenuTitle, return self.c.WithWaitingStatus(self.c.Tr.CreatingTag, func(gocui.Task) error {
Items: []*types.MenuItem{ if description != "" {
{ self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag)
Label: self.c.Tr.LightweightTag, if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, description); err != nil {
OnPress: func() error { return self.c.Error(err)
return self.handleCreateLightweightTag(ref, onCreate) }
}, } else {
}, self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag)
{ if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil {
Label: self.c.Tr.AnnotatedTag, return self.c.Error(err)
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)
} }
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( return self.commitsHelper.OpenCommitMessagePanel(
&OpenCommitMessagePanelOpts{ &OpenCommitMessagePanelOpts{
CommitIndex: context.NoCommitIndex, CommitIndex: context.NoCommitIndex,
InitialMessage: initialMessage, InitialMessage: initialMessage,
Title: self.c.Tr.CommitSummary, SummaryTitle: self.c.Tr.CommitSummaryTitle,
PreserveMessage: true, DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
OnConfirm: self.handleCommit, PreserveMessage: true,
OnConfirm: self.handleCommit,
}, },
) )
} }
func (self *WorkingTreeHelper) handleCommit(message string) error { func (self *WorkingTreeHelper) handleCommit(summary string, description string) error {
cmdObj := self.c.Git().Commit.CommitCmdObj(message) cmdObj := self.c.Git().Commit.CommitCmdObj(summary, description)
self.c.LogAction(self.c.Tr.Actions.Commit) self.c.LogAction(self.c.Tr.Actions.Commit)
_ = self.commitsHelper.PopCommitMessageContexts()
return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error { return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error {
self.commitsHelper.OnCommitSuccess() self.commitsHelper.OnCommitSuccess()
return nil return nil
@@ -267,22 +267,22 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error {
return self.c.Helpers().Commits.OpenCommitMessagePanel( return self.c.Helpers().Commits.OpenCommitMessagePanel(
&helpers.OpenCommitMessagePanelOpts{ &helpers.OpenCommitMessagePanelOpts{
CommitIndex: self.context().GetSelectedLineIdx(), CommitIndex: self.context().GetSelectedLineIdx(),
InitialMessage: commitMessage, InitialMessage: commitMessage,
Title: self.c.Tr.Actions.RewordCommit, SummaryTitle: self.c.Tr.Actions.RewordCommit,
PreserveMessage: false, DescriptionTitle: self.c.Tr.CommitDescriptionTitle,
OnConfirm: self.handleReword, PreserveMessage: false,
OnConfirm: self.handleReword,
}, },
) )
} }
func (self *LocalCommitsController) handleReword(message string) error { func (self *LocalCommitsController) handleReword(summary string, description string) error {
err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), message) err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), summary, description)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
self.c.Helpers().Commits.OnCommitSuccess() self.c.Helpers().Commits.OnCommitSuccess()
_ = self.c.Helpers().Commits.PopCommitMessageContexts()
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) 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 { 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 { func (self *LocalCommitsController) openSearch() error {
+3 -1
View File
@@ -53,7 +53,9 @@ func (self *MenuController) GetOnClick() func() error {
func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error {
return func(types.OnFocusOpts) error { return func(types.OnFocusOpts) error {
selectedMenuItem := self.context().GetSelected() selectedMenuItem := self.context().GetSelected()
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip) if selectedMenuItem != nil {
self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip)
}
return nil return nil
} }
} }
+5 -3
View File
@@ -4,6 +4,7 @@ import (
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
@@ -37,9 +38,10 @@ func (self *OptionsMenuAction) Call() error {
}) })
return self.c.Menu(types.CreateMenuOptions{ return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Keybindings, Title: self.c.Tr.Keybindings,
Items: menuItems, Items: menuItems,
HideCancel: true, HideCancel: true,
ColumnAlignment: []utils.Alignment{utils.AlignRight, utils.AlignLeft},
}) })
} }
@@ -70,6 +70,7 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
} }
self.setSubCommits(commits) self.setSubCommits(commits)
self.c.Helpers().Refresh.RefreshAuthors(commits)
subCommitsContext := self.c.Contexts().SubCommits subCommitsContext := self.c.Contexts().SubCommits
subCommitsContext.SetSelectedLineIdx(0) subCommitsContext.SetSelectedLineIdx(0)
+1 -1
View File
@@ -141,7 +141,7 @@ func (self *TagsController) createResetMenu(tag *models.Tag) error {
func (self *TagsController) create() error { func (self *TagsController) create() error {
// leaving commit SHA blank so that we're just creating the tag for the current commit // 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 { func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error {
+2
View File
@@ -349,6 +349,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
ReflogCommits: make([]*models.Commit, 0), ReflogCommits: make([]*models.Commit, 0),
BisectInfo: git_commands.NewNullBisectInfo(), BisectInfo: git_commands.NewNullBisectInfo(),
FilesTrie: patricia.NewTrie(), FilesTrie: patricia.NewTrie(),
Authors: map[string]*models.Author{},
}, },
Modes: &types.Modes{ Modes: &types.Modes{
Filtering: filtering.New(startArgs.FilterPath), Filtering: filtering.New(startArgs.FilterPath),
@@ -456,6 +457,7 @@ func NewGui(
SyncMutex: &deadlock.Mutex{}, SyncMutex: &deadlock.Mutex{},
LocalCommitsMutex: &deadlock.Mutex{}, LocalCommitsMutex: &deadlock.Mutex{},
SubCommitsMutex: &deadlock.Mutex{}, SubCommitsMutex: &deadlock.Mutex{},
AuthorsMutex: &deadlock.Mutex{},
SubprocessMutex: &deadlock.Mutex{}, SubprocessMutex: &deadlock.Mutex{},
PopupMutex: &deadlock.Mutex{}, PopupMutex: &deadlock.Mutex{},
PtyMutex: &deadlock.Mutex{}, PtyMutex: &deadlock.Mutex{},
+1 -1
View File
@@ -41,7 +41,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
} }
} }
gui.State.Contexts.Menu.SetMenuItems(opts.Items) gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment)
gui.State.Contexts.Menu.SetSelectedLineIdx(0) gui.State.Contexts.Menu.SetSelectedLineIdx(0)
gui.Views.Menu.Title = opts.Title gui.Views.Menu.Title = opts.Title
+7 -3
View File
@@ -133,9 +133,10 @@ type IPopupHandler interface {
} }
type CreateMenuOptions struct { type CreateMenuOptions struct {
Title string Title string
Items []*MenuItem Items []*MenuItem
HideCancel bool HideCancel bool
ColumnAlignment []utils.Alignment
} }
type CreatePopupPanelOpts struct { type CreatePopupPanelOpts struct {
@@ -216,6 +217,8 @@ type Model struct {
// for displaying suggestions while typing in a file name // for displaying suggestions while typing in a file name
FilesTrie *patricia.Trie 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 // if you add a new mutex here be sure to instantiate it. We're using pointers to
@@ -227,6 +230,7 @@ type Mutexes struct {
SyncMutex *deadlock.Mutex SyncMutex *deadlock.Mutex
LocalCommitsMutex *deadlock.Mutex LocalCommitsMutex *deadlock.Mutex
SubCommitsMutex *deadlock.Mutex SubCommitsMutex *deadlock.Mutex
AuthorsMutex *deadlock.Mutex
SubprocessMutex *deadlock.Mutex SubprocessMutex *deadlock.Mutex
PopupMutex *deadlock.Mutex PopupMutex *deadlock.Mutex
PtyMutex *deadlock.Mutex PtyMutex *deadlock.Mutex
+3 -4
View File
@@ -181,7 +181,7 @@ func chineseTranslationSet() TranslationSet {
RecentRepos: "最近的仓库", RecentRepos: "最近的仓库",
MergeOptionsTitle: "合并选项", MergeOptionsTitle: "合并选项",
RebaseOptionsTitle: "变基选项", RebaseOptionsTitle: "变基选项",
CommitMessageTitle: "提交讯息", CommitSummaryTitle: "提交讯息",
LocalBranchesTitle: "分支页面", LocalBranchesTitle: "分支页面",
SearchTitle: "搜索", SearchTitle: "搜索",
TagsTitle: "标签页面", TagsTitle: "标签页面",
@@ -305,8 +305,8 @@ func chineseTranslationSet() TranslationSet {
EditRemote: "编辑远程仓库", EditRemote: "编辑远程仓库",
TagCommit: "标签提交", TagCommit: "标签提交",
TagMenuTitle: "创建标签", TagMenuTitle: "创建标签",
TagNameTitle: "标签名称:", TagNameTitle: "标签名称",
TagMessageTitle: "标签消息:", TagMessageTitle: "标签消息",
AnnotatedTag: "附注标签", AnnotatedTag: "附注标签",
LightweightTag: "轻量标签", LightweightTag: "轻量标签",
DeleteTag: "删除标签", DeleteTag: "删除标签",
@@ -315,7 +315,6 @@ func chineseTranslationSet() TranslationSet {
PushTagTitle: "将 {{.tagName}} 推送到远程仓库:", PushTagTitle: "将 {{.tagName}} 推送到远程仓库:",
PushTag: "推送标签", PushTag: "推送标签",
CreateTag: "创建标签", CreateTag: "创建标签",
CreateTagTitle: "标签名称:",
FetchRemote: "抓取远程仓库", FetchRemote: "抓取远程仓库",
FetchingRemoteStatus: "抓取远程仓库中", FetchingRemoteStatus: "抓取远程仓库中",
CheckoutCommit: "检出提交", CheckoutCommit: "检出提交",
+2 -3
View File
@@ -146,7 +146,7 @@ func dutchTranslationSet() TranslationSet {
RecentRepos: "Recente repositories", RecentRepos: "Recente repositories",
MergeOptionsTitle: "Merge opties", MergeOptionsTitle: "Merge opties",
RebaseOptionsTitle: "Rebase opties", RebaseOptionsTitle: "Rebase opties",
CommitMessageTitle: "Commit bericht", CommitSummaryTitle: "Commit bericht",
LocalBranchesTitle: "Branches", LocalBranchesTitle: "Branches",
SearchTitle: "Zoek", SearchTitle: "Zoek",
TagsTitle: "Tags", TagsTitle: "Tags",
@@ -263,14 +263,13 @@ func dutchTranslationSet() TranslationSet {
SetUpstreamMessage: "Weet je zeker dat je de upstream branch van '{{.checkedOut}}' naar '{{.selected}}' wilt zetten", SetUpstreamMessage: "Weet je zeker dat je de upstream branch van '{{.checkedOut}}' naar '{{.selected}}' wilt zetten",
EditRemote: "Wijzig remote", EditRemote: "Wijzig remote",
TagCommit: "Tag commit", TagCommit: "Tag commit",
TagNameTitle: "Tag naam:", TagNameTitle: "Tag naam",
DeleteTag: "Verwijder tag", DeleteTag: "Verwijder tag",
DeleteTagTitle: "Verwijder tag", DeleteTagTitle: "Verwijder tag",
DeleteTagPrompt: "Weet je zeker dat je '{{.tagName}}' wil verwijderen?", DeleteTagPrompt: "Weet je zeker dat je '{{.tagName}}' wil verwijderen?",
PushTagTitle: "Remote om tag '{{.tagName}}' te pushen naar:", PushTagTitle: "Remote om tag '{{.tagName}}' te pushen naar:",
PushTag: "Push tag", PushTag: "Push tag",
CreateTag: "Creëer tag", CreateTag: "Creëer tag",
CreateTagTitle: "Tag naam:",
FetchRemote: "Fetch remote", FetchRemote: "Fetch remote",
FetchingRemoteStatus: "Remote fetchen", FetchingRemoteStatus: "Remote fetchen",
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",
+6 -6
View File
@@ -192,7 +192,7 @@ type TranslationSet struct {
RecentRepos string RecentRepos string
MergeOptionsTitle string MergeOptionsTitle string
RebaseOptionsTitle string RebaseOptionsTitle string
CommitMessageTitle string CommitSummaryTitle string
CommitDescriptionTitle string CommitDescriptionTitle string
CommitDescriptionSubTitle string CommitDescriptionSubTitle string
LocalBranchesTitle string LocalBranchesTitle string
@@ -355,7 +355,7 @@ type TranslationSet struct {
PushTagTitle string PushTagTitle string
PushTag string PushTag string
CreateTag string CreateTag string
CreateTagTitle string CreatingTag string
FetchRemote string FetchRemote string
FetchingRemoteStatus string FetchingRemoteStatus string
CheckoutCommit string CheckoutCommit string
@@ -884,7 +884,7 @@ func EnglishTranslationSet() TranslationSet {
RecentRepos: "Recent repositories", RecentRepos: "Recent repositories",
MergeOptionsTitle: "Merge options", MergeOptionsTitle: "Merge options",
RebaseOptionsTitle: "Rebase options", RebaseOptionsTitle: "Rebase options",
CommitMessageTitle: "Commit summary", CommitSummaryTitle: "Commit summary",
CommitDescriptionTitle: "Commit description", CommitDescriptionTitle: "Commit description",
CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus", CommitDescriptionSubTitle: "Press {{.togglePanelKeyBinding}} to toggle focus",
LocalBranchesTitle: "Local branches", LocalBranchesTitle: "Local branches",
@@ -1039,8 +1039,8 @@ func EnglishTranslationSet() TranslationSet {
EditRemote: "Edit remote", EditRemote: "Edit remote",
TagCommit: "Tag commit", TagCommit: "Tag commit",
TagMenuTitle: "Create tag", TagMenuTitle: "Create tag",
TagNameTitle: "Tag name:", TagNameTitle: "Tag name",
TagMessageTitle: "Tag message:", TagMessageTitle: "Tag description",
AnnotatedTag: "Annotated tag", AnnotatedTag: "Annotated tag",
LightweightTag: "Lightweight tag", LightweightTag: "Lightweight tag",
DeleteTag: "Delete tag", DeleteTag: "Delete tag",
@@ -1049,7 +1049,7 @@ func EnglishTranslationSet() TranslationSet {
PushTagTitle: "Remote to push tag '{{.tagName}}' to:", PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
PushTag: "Push tag", PushTag: "Push tag",
CreateTag: "Create tag", CreateTag: "Create tag",
CreateTagTitle: "Tag name:", CreatingTag: "Creating tag",
FetchRemote: "Fetch remote", FetchRemote: "Fetch remote",
FetchingRemoteStatus: "Fetching remote", FetchingRemoteStatus: "Fetching remote",
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",
+3 -4
View File
@@ -184,7 +184,7 @@ func japaneseTranslationSet() TranslationSet {
RecentRepos: "最近使用したリポジトリ", RecentRepos: "最近使用したリポジトリ",
// MergeOptionsTitle: "Merge Options", // MergeOptionsTitle: "Merge Options",
// RebaseOptionsTitle: "Rebase Options", // RebaseOptionsTitle: "Rebase Options",
CommitMessageTitle: "コミットメッセージ", CommitSummaryTitle: "コミットメッセージ",
LocalBranchesTitle: "ブランチ", LocalBranchesTitle: "ブランチ",
SearchTitle: "検索", SearchTitle: "検索",
TagsTitle: "タグ", TagsTitle: "タグ",
@@ -315,8 +315,8 @@ func japaneseTranslationSet() TranslationSet {
EditRemote: "リモートを編集", EditRemote: "リモートを編集",
TagCommit: "タグを作成", TagCommit: "タグを作成",
TagMenuTitle: "タグを作成", TagMenuTitle: "タグを作成",
TagNameTitle: "タグ名:", TagNameTitle: "タグ名",
TagMessageTitle: "タグメッセージ: ", TagMessageTitle: "タグメッセージ",
AnnotatedTag: "注釈付きタグ", AnnotatedTag: "注釈付きタグ",
LightweightTag: "軽量タグ", LightweightTag: "軽量タグ",
DeleteTag: "タグを削除", DeleteTag: "タグを削除",
@@ -325,7 +325,6 @@ func japaneseTranslationSet() TranslationSet {
PushTagTitle: "リモートにタグ '{{.tagName}}' をpush", PushTagTitle: "リモートにタグ '{{.tagName}}' をpush",
PushTag: "タグをpush", PushTag: "タグをpush",
CreateTag: "タグを作成", CreateTag: "タグを作成",
CreateTagTitle: "タグ名:",
FetchRemote: "リモートをfetch", FetchRemote: "リモートをfetch",
FetchingRemoteStatus: "リモートをfetch", FetchingRemoteStatus: "リモートをfetch",
CheckoutCommit: "コミットをチェックアウト", CheckoutCommit: "コミットをチェックアウト",
+3 -4
View File
@@ -182,7 +182,7 @@ func koreanTranslationSet() TranslationSet {
RecentRepos: "최근에 사용한 저장소", RecentRepos: "최근에 사용한 저장소",
MergeOptionsTitle: "Merge options", MergeOptionsTitle: "Merge options",
RebaseOptionsTitle: "Rebase options", RebaseOptionsTitle: "Rebase options",
CommitMessageTitle: "커밋메시지", CommitSummaryTitle: "커밋메시지",
LocalBranchesTitle: "브랜치", LocalBranchesTitle: "브랜치",
SearchTitle: "검색", SearchTitle: "검색",
TagsTitle: "태그", TagsTitle: "태그",
@@ -310,8 +310,8 @@ func koreanTranslationSet() TranslationSet {
EditRemote: "Remote를 수정", EditRemote: "Remote를 수정",
TagCommit: "Tag commit", TagCommit: "Tag commit",
TagMenuTitle: "태그 작성", TagMenuTitle: "태그 작성",
TagNameTitle: "태그 이름:", TagNameTitle: "태그 이름",
TagMessageTitle: "태그 메시지: ", TagMessageTitle: "태그 메시지",
AnnotatedTag: "Annotated tag", AnnotatedTag: "Annotated tag",
LightweightTag: "Lightweight tag", LightweightTag: "Lightweight tag",
DeleteTag: "태그 삭제", DeleteTag: "태그 삭제",
@@ -320,7 +320,6 @@ func koreanTranslationSet() TranslationSet {
PushTagTitle: "원격에 태그 '{{.tagName}}' 를 푸시", PushTagTitle: "원격에 태그 '{{.tagName}}' 를 푸시",
PushTag: "태그를 push", PushTag: "태그를 push",
CreateTag: "태그를 생성", CreateTag: "태그를 생성",
CreateTagTitle: "태그 이름:",
FetchRemote: "원격을 업데이트", FetchRemote: "원격을 업데이트",
FetchingRemoteStatus: "원격을 업데이트 중", FetchingRemoteStatus: "원격을 업데이트 중",
CheckoutCommit: "커밋을 체크아웃", CheckoutCommit: "커밋을 체크아웃",
+3 -4
View File
@@ -216,7 +216,7 @@ func RussianTranslationSet() TranslationSet {
RecentRepos: "Последние репозитории", RecentRepos: "Последние репозитории",
MergeOptionsTitle: "Параметры слияния", MergeOptionsTitle: "Параметры слияния",
RebaseOptionsTitle: "Параметры перебазирования", RebaseOptionsTitle: "Параметры перебазирования",
CommitMessageTitle: "Сводка коммита", CommitSummaryTitle: "Сводка коммита",
CommitDescriptionTitle: "Описание коммита", CommitDescriptionTitle: "Описание коммита",
CommitDescriptionSubTitle: "Нажмите вкладку, чтобы переключить фокус", CommitDescriptionSubTitle: "Нажмите вкладку, чтобы переключить фокус",
LocalBranchesTitle: "Локальные Ветки", LocalBranchesTitle: "Локальные Ветки",
@@ -371,8 +371,8 @@ func RussianTranslationSet() TranslationSet {
EditRemote: "Редактировать удалённый репозитории", EditRemote: "Редактировать удалённый репозитории",
TagCommit: "Пометить коммит тегом", TagCommit: "Пометить коммит тегом",
TagMenuTitle: "Создать тег", TagMenuTitle: "Создать тег",
TagNameTitle: "Название тега:", TagNameTitle: "Название тега",
TagMessageTitle: "Сообщения тега:", TagMessageTitle: "Сообщения тега",
AnnotatedTag: "Аннотированный тег", AnnotatedTag: "Аннотированный тег",
LightweightTag: "Легковесный тег", LightweightTag: "Легковесный тег",
DeleteTag: "Удалить тег", DeleteTag: "Удалить тег",
@@ -381,7 +381,6 @@ func RussianTranslationSet() TranslationSet {
PushTagTitle: "Удалённый репозитории для отправки тега '{{.tagName}}' в:", PushTagTitle: "Удалённый репозитории для отправки тега '{{.tagName}}' в:",
PushTag: "Отправить тег", PushTag: "Отправить тег",
CreateTag: "Создать тег", CreateTag: "Создать тег",
CreateTagTitle: "Название тега:",
FetchRemote: "Получение изменения из удалённого репозитория", FetchRemote: "Получение изменения из удалённого репозитория",
FetchingRemoteStatus: "Получение статуса удалённого репозитория", FetchingRemoteStatus: "Получение статуса удалённого репозитория",
CheckoutCommit: "Переключить коммит", CheckoutCommit: "Переключить коммит",
+3 -4
View File
@@ -247,7 +247,7 @@ func traditionalChineseTranslationSet() TranslationSet {
RecentRepos: "最近的版本庫", RecentRepos: "最近的版本庫",
MergeOptionsTitle: "合併選項", MergeOptionsTitle: "合併選項",
RebaseOptionsTitle: "變基選項", RebaseOptionsTitle: "變基選項",
CommitMessageTitle: "提交摘要", CommitSummaryTitle: "提交摘要",
CommitDescriptionTitle: "提交描述", CommitDescriptionTitle: "提交描述",
CommitDescriptionSubTitle: "按 tab 切換焦點", CommitDescriptionSubTitle: "按 tab 切換焦點",
LocalBranchesTitle: "本地分支", LocalBranchesTitle: "本地分支",
@@ -398,8 +398,8 @@ func traditionalChineseTranslationSet() TranslationSet {
EditRemote: "編輯遠端", EditRemote: "編輯遠端",
TagCommit: "打標籤到提交", TagCommit: "打標籤到提交",
TagMenuTitle: "建立標籤", TagMenuTitle: "建立標籤",
TagNameTitle: "標籤名稱:", TagNameTitle: "標籤名稱",
TagMessageTitle: "標籤訊息:", TagMessageTitle: "標籤訊息",
AnnotatedTag: "附註標籤", AnnotatedTag: "附註標籤",
LightweightTag: "輕量標籤", LightweightTag: "輕量標籤",
DeleteTag: "刪除標籤", DeleteTag: "刪除標籤",
@@ -408,7 +408,6 @@ func traditionalChineseTranslationSet() TranslationSet {
PushTagTitle: "推送標籤 '{{.tagName}}' 至遠端:", PushTagTitle: "推送標籤 '{{.tagName}}' 至遠端:",
PushTag: "推送標籤", PushTag: "推送標籤",
CreateTag: "建立標籤", CreateTag: "建立標籤",
CreateTagTitle: "標籤名稱:",
FetchRemote: "擷取遠端", FetchRemote: "擷取遠端",
FetchingRemoteStatus: "正在擷取遠端", FetchingRemoteStatus: "正在擷取遠端",
CheckoutCommit: "檢出提交", CheckoutCommit: "檢出提交",
@@ -23,3 +23,9 @@ func (self *CommitDescriptionPanelDriver) AddNewline() *CommitDescriptionPanelDr
self.t.press(self.t.keys.Universal.Confirm) self.t.press(self.t.keys.Universal.Confirm)
return self return self
} }
func (self *CommitDescriptionPanelDriver) Title(expected *TextMatcher) *CommitDescriptionPanelDriver {
self.getViewDriver().Title(expected)
return self
}
@@ -539,6 +539,7 @@ func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
self.Press(self.t.keys.Universal.StartSearch). self.Press(self.t.keys.Universal.StartSearch).
Tap(func() { Tap(func() {
self.t.ExpectSearch(). self.t.ExpectSearch().
Clear().
Type(text). Type(text).
Confirm() Confirm()
+2 -7
View File
@@ -26,13 +26,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem(). SelectNextItem().
Press(keys.Branches.CreateTag) Press(keys.Branches.CreateTag)
t.ExpectPopup().Menu(). t.ExpectPopup().CommitMessagePanel().
Title(Equals("Create tag")). Title(Equals("Tag name")).
Select(Contains("Lightweight")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Tag name:")).
Type("new-tag"). Type("new-tag").
Confirm() Confirm()
+2 -7
View File
@@ -23,13 +23,8 @@ var CreateTag = NewIntegrationTest(NewIntegrationTestArgs{
). ).
Press(keys.Commits.CreateTag) Press(keys.Commits.CreateTag)
t.ExpectPopup().Menu(). t.ExpectPopup().CommitMessagePanel().
Title(Equals("Create tag")). Title(Equals("Tag name")).
Select(Contains("Lightweight")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Tag name:")).
Type("new-tag"). Type("new-tag").
Confirm() Confirm()
+32 -4
View File
@@ -5,29 +5,58 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components" . "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{ var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Set author on a commit", Description: "Set author on a commit",
ExtraCmdArgs: []string{}, ExtraCmdArgs: []string{},
Skip: false, Skip: false,
SetupConfig: func(config *config.AppConfig) {}, SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) { SetupRepo: func(shell *Shell) {
shell.NewBranch("original")
shell.SetConfig("user.email", "Bill@example.com") shell.SetConfig("user.email", "Bill@example.com")
shell.SetConfig("user.name", "Bill Smith") shell.SetConfig("user.name", "Bill Smith")
shell.EmptyCommit("one") shell.EmptyCommit("one")
shell.NewBranch("other")
shell.SetConfig("user.email", "John@example.com") shell.SetConfig("user.email", "John@example.com")
shell.SetConfig("user.name", "John Smith") shell.SetConfig("user.name", "John Smith")
shell.EmptyCommit("two") shell.EmptyCommit("two")
shell.Checkout("original")
}, },
Run: func(t *TestDriver, keys config.KeybindingConfig) { Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits(). t.Views().Commits().
Focus(). 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( Lines(
Contains("JS").Contains("two").IsSelected(), Contains("JS").Contains("two").IsSelected(),
Contains("BS").Contains("one"), Contains("BS").Contains("one"),
). )
t.Views().Commits().
Focus().
Press(keys.Commits.ResetCommitAuthor). Press(keys.Commits.ResetCommitAuthor).
Tap(func() { Tap(func() {
t.ExpectPopup().Menu(). t.ExpectPopup().Menu().
@@ -38,14 +67,13 @@ var SetAuthor = NewIntegrationTest(NewIntegrationTestArgs{
t.ExpectPopup().Prompt(). t.ExpectPopup().Prompt().
Title(Contains("Set author")). Title(Contains("Set author")).
SuggestionLines( SuggestionLines(
Contains("John Smith"),
Contains("Bill Smith"), Contains("Bill Smith"),
Contains("John Smith"),
). ).
ConfirmSuggestion(Contains("John Smith")) ConfirmSuggestion(Contains("John Smith"))
}). }).
Lines( Lines(
Contains("JS").Contains("two").IsSelected(), Contains("JS").Contains("one").IsSelected(),
Contains("BS").Contains("one"),
) )
}, },
}) })
@@ -0,0 +1,35 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterFuzzy = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that fuzzy filtering works (not just exact matches)",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("this-is-my-branch")
shell.EmptyCommit("first commit")
shell.NewBranch("other-branch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains(`other-branch`).IsSelected(),
Contains(`this-is-my-branch`),
).
FilterOrSearch("timb"). // using first letters of words
Lines(
Contains(`this-is-my-branch`).IsSelected(),
).
FilterOrSearch("brnch"). // allows missing letter
Lines(
Contains(`other-branch`).IsSelected(),
Contains(`this-is-my-branch`),
)
},
})
@@ -0,0 +1,37 @@
package tag
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CreateWhileCommitting = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Draft a commit message, escape out, and make a tag. Verify the draft message doesn't appear in the tag create prompt",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("initial commit")
shell.CreateFileAndAdd("file.txt", "file contents")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Press(keys.Files.CommitChanges).
Tap(func() {
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
Type("draft message").
Cancel()
})
t.Views().Tags().
Focus().
IsEmpty().
Press(keys.Universal.New).
Tap(func() {
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Tag name")).
InitialText(Equals(""))
})
},
})
+13 -12
View File
@@ -19,19 +19,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
IsEmpty(). IsEmpty().
Press(keys.Universal.New). Press(keys.Universal.New).
Tap(func() { Tap(func() {
t.ExpectPopup().Menu(). t.ExpectPopup().CommitMessagePanel().
Title(Equals("Create tag")). Title(Equals("Tag name")).
Select(Contains("Annotated")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Tag name:")).
Type("new-tag"). Type("new-tag").
Confirm() SwitchToDescription().
Title(Equals("Tag description")).
t.ExpectPopup().Prompt().
Title(Equals("Tag message:")).
Type("message"). Type("message").
SwitchToSummary().
Confirm() Confirm()
}). }).
Lines( Lines(
@@ -44,6 +38,13 @@ var CrudAnnotated = NewIntegrationTest(NewIntegrationTestArgs{
Content(Equals("Are you sure you want to delete tag 'new-tag'?")). Content(Equals("Are you sure you want to delete tag 'new-tag'?")).
Confirm() 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(). IsEmpty().
Press(keys.Universal.New). Press(keys.Universal.New).
Tap(func() { Tap(func() {
t.ExpectPopup().Menu(). t.ExpectPopup().CommitMessagePanel().
Title(Equals("Create tag")). Title(Equals("Tag name")).
Select(Contains("Lightweight")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("Tag name:")).
Type("new-tag"). Type("new-tag").
Confirm() Confirm()
}). }).
+3
View File
@@ -98,6 +98,7 @@ var tests = []*components.IntegrationTest{
file.RememberCommitMessageAfterFail, file.RememberCommitMessageAfterFail,
filter_and_search.FilterCommitFiles, filter_and_search.FilterCommitFiles,
filter_and_search.FilterFiles, filter_and_search.FilterFiles,
filter_and_search.FilterFuzzy,
filter_and_search.FilterMenu, filter_and_search.FilterMenu,
filter_and_search.FilterRemoteBranches, filter_and_search.FilterRemoteBranches,
filter_and_search.NestedFilter, filter_and_search.NestedFilter,
@@ -206,11 +207,13 @@ var tests = []*components.IntegrationTest{
sync.PushWithCredentialPrompt, sync.PushWithCredentialPrompt,
sync.RenameBranchAndPull, sync.RenameBranchAndPull,
tag.Checkout, tag.Checkout,
tag.CreateWhileCommitting,
tag.CrudAnnotated, tag.CrudAnnotated,
tag.CrudLightweight, tag.CrudLightweight,
tag.Reset, tag.Reset,
ui.Accordion, ui.Accordion,
ui.DoublePopup, ui.DoublePopup,
ui.EmptyMenu,
ui.SwitchTabFromMenu, ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop, undo.UndoCheckoutAndDrop,
undo.UndoDrop, undo.UndoDrop,
+31
View File
@@ -0,0 +1,31 @@
package ui
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var EmptyMenu = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that we don't crash on an empty menu",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Press(keys.Universal.OptionMenu)
t.Views().Menu().
IsFocused().
// a string that filters everything out
FilterOrSearch("ljasldkjaslkdjalskdjalsdjaslkd").
IsEmpty().
Press(keys.Universal.Select)
// back in the files view, selecting the non-existing menu item was a no-op
t.Views().Files().
IsFocused()
},
})
+30 -3
View File
@@ -164,6 +164,29 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines) 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 loaded := false
go utils.Safe(func() { go utils.Safe(func() {
@@ -203,13 +226,15 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
break outer break outer
case linesToRead := <-self.readLines: case linesToRead := <-self.readLines:
for i := 0; i < linesToRead.Total; i++ { for i := 0; i < linesToRead.Total; i++ {
var ok bool
var line []byte
select { select {
case <-opts.Stop: case <-opts.Stop:
break outer break outer
default: case line, ok = <-lineChan:
break
} }
ok := scanner.Scan()
loadingMutex.Lock() loadingMutex.Lock()
if !loaded { if !loaded {
self.beforeStart() self.beforeStart()
@@ -226,7 +251,8 @@ func (self *ViewBufferManager) NewCmdTask(start func() (*exec.Cmd, io.Reader), p
self.onEndOfInput() self.onEndOfInput()
break outer break outer
} }
writeToView(append(scanner.Bytes(), '\n')) writeToView(append(line, '\n'))
lineWrittenChan <- struct{}{}
if i+1 == linesToRead.InitialRefreshAfter { if i+1 == linesToRead.InitialRefreshAfter {
// We have read enough lines to fill the view, so do a first refresh // 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() onDone()
close(done) close(done)
close(lineWrittenChan)
}) })
self.readLines <- linesToRead self.readLines <- linesToRead
+42 -4
View File
@@ -1265,14 +1265,52 @@ func lineWrap(line []cell, columns int) [][]cell {
var n int var n int
var offset int var offset int
lastWhitespaceIndex := -1
lines := make([][]cell, 0, 1) lines := make([][]cell, 0, 1)
for i := range line { for i := range line {
rw := runewidth.RuneWidth(line[i].chr) currChr := line[i].chr
rw := runewidth.RuneWidth(currChr)
n += rw n += rw
// if currChr == 'g' {
// panic(n)
// }
if n > columns { if n > columns {
n = rw // This code is convoluted but we've got comprehensive tests so feel free to do whatever you want
lines = append(lines, line[offset:i]) // to the code to simplify it so long as our tests still pass.
offset = i if currChr == ' ' {
// if the line ends in a space, we'll omit it. This means there'll be no
// way to distinguish between a clean break and a mid-word break, but
// I think it's worth it.
lines = append(lines, line[offset:i])
offset = i + 1
n = 0
} else if currChr == '-' {
// if the last character is hyphen and the width of line is equal to the columns
lines = append(lines, line[offset:i])
offset = i
n = rw
} else if lastWhitespaceIndex != -1 && lastWhitespaceIndex+1 != i {
// if there is a space in the line and the line is not breaking at a space/hyphen
if line[lastWhitespaceIndex].chr == '-' {
// if break occurs at hyphen, we'll retain the hyphen
lines = append(lines, line[offset:lastWhitespaceIndex+1])
offset = lastWhitespaceIndex + 1
n = i - offset
} else {
// if break occurs at space, we'll omit the space
lines = append(lines, line[offset:lastWhitespaceIndex])
offset = lastWhitespaceIndex + 1
n = i - offset + 1
}
} else {
// in this case we're breaking mid-word
lines = append(lines, line[offset:i])
offset = i
n = rw
}
lastWhitespaceIndex = -1
} else if line[i].chr == ' ' || line[i].chr == '-' {
lastWhitespaceIndex = i
} }
} }
+1 -1
View File
@@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20230719120401-398f4965241f # github.com/jesseduffield/gocui v0.3.1-0.20230723014157-03e858e46144
## explicit; go 1.12 ## explicit; go 1.12
github.com/jesseduffield/gocui github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10