Compare commits

..
53 Commits
Author SHA1 Message Date
Dawid Dziurla f42a595aba Merge pull request #1130 from jesseduffield/dawidd6-patch-2
gui: ReplaceAll -> Replace
2020-12-24 10:24:49 +01:00
Dawid Dziurla 797722ec12 Merge pull request #1129 from jesseduffield/dawidd6-patch-1
workflows: split CD into separate jobs
2020-12-24 10:24:30 +01:00
Dawid Dziurla bb4bf23c5c gui: ReplaceAll -> Replace 2020-12-24 10:21:54 +01:00
Dawid Dziurla f3aacbd253 workflows: split CD into separate jobs 2020-12-24 09:51:50 +01:00
Jeff Hertzler 106fce26b5 Update Custom_Command_Keybindings.md 2020-12-23 18:54:26 -08:00
Jesse Duffield caf208b0a4 v24 release notes 2020-12-24 13:52:29 +11:00
Jesse Duffield 13b9a8bc9a add integration test for branch checkout autocomplete 2020-11-28 20:48:17 +11:00
Jesse Duffield 14ce230683 refactor 2020-11-28 20:48:17 +11:00
Jesse Duffield f31fbc10f6 soft code finding of suggestions 2020-11-28 20:48:17 +11:00
Jesse Duffield be404068ff support labels for suggestions which are distinct from values 2020-11-28 20:48:17 +11:00
Jesse Duffield 5671ec5f58 refactor prompt opts 2020-11-28 20:48:17 +11:00
Jesse Duffield da3b0bf7c8 Start on supporting auto-suggestions when checking out a branch
switch to other fuzzy package with no dependencies
2020-11-28 20:48:17 +11:00
Yuki Osaki 90ade3225f Add lc prefix 2020-11-28 19:19:47 +11:00
Yuki Osaki 4928d1d490 Visualize the commits for all branches 2020-11-28 19:19:47 +11:00
Kalvin Pearce 9c52eb9d6f Add notARepository description to config docs 2020-11-28 10:51:34 +11:00
Kalvin Pearce 0a58cb2877 Fix formatting on notARepository checks 2020-11-28 10:51:34 +11:00
Kalvin Pearce 7581830e70 Add notARepository to config docs 2020-11-28 10:51:34 +11:00
Kalvin Pearce d468866746 Add config option for notInRepo behaviour. 2020-11-28 10:51:34 +11:00
Jesse Duffield 999e170f1d standardise how we read from the config 2020-11-28 10:45:30 +11:00
Nick Flueckiger 7513bfb13a Implement suggestions 2020-11-28 10:42:38 +11:00
Nick Flueckiger 1f27002b84 Switch the directory check 2020-11-28 10:42:38 +11:00
Nick Flueckiger 669bfe763a A small change that enables direct lazygit directory config 2020-11-28 10:42:38 +11:00
Davyd McColl 860370a845 👌 update as per PR commentary 2020-11-28 10:27:28 +11:00
Davyd McColl 196761a40a 🐛 should only stage all if configured to do so _and_ there are no items staged 2020-11-28 10:27:28 +11:00
Davyd McColl 26d5444919 ✨ implement quick commit when no files staged, if configured to do so 2020-11-28 10:27:28 +11:00
Nathan Bell e05c41828c added tests and fixed bug found in tests 2020-11-25 08:41:22 +11:00
Nathan Bell c4cce58464 Allow --follow-tags to be disabled if push.followTags is configured to false 2020-11-25 08:41:22 +11:00
Jesse Duffield f7e6d4e724 fix updater 2020-11-22 10:00:35 +11:00
Jesse Duffield d02e992265 Update Custom_Command_Keybindings.md 2020-11-21 17:35:51 +11:00
Jesse Duffield 3e13936e08 notify user upon copying something to clipboard 2020-11-21 17:31:08 +11:00
Jesse Duffield a3dfcd5a95 toast notifications 2020-11-21 17:31:08 +11:00
Jesse Duffield ce928dc6c8 Update docs/README.md 2020-11-21 14:14:40 +11:00
Nils Andresen 1dea988cd6 Added a reference to chocolatey in README
and also added a simple overview of documentation under docs/README.md
2020-11-21 14:14:40 +11:00
Farzad Majidfayyaz 74bb6f0012 Change copy PR mapping to <c-y> and use gui.Tr for the message 2020-11-19 09:43:51 +11:00
Farzad Majidfayyaz 79888d3bde Add mapping to copy a pull request URL to the clipboard 2020-11-19 09:43:51 +11:00
Jaime Gomes 4e1d3e45a3 add minimum macos version 10.10 to the README 2020-11-18 08:36:50 +11:00
Jesse Duffield 682db77401 fix lint errors 2020-11-18 08:36:19 +11:00
Dawid Dziurla 6faed08d9d workflows: clean up linting 2020-11-16 10:02:57 +11:00
Sean Stiglitz 62b200a4be Add GolangCI action. 2020-11-16 10:02:57 +11:00
Dawid Dziurla f7bab5fdc0 workflows: update apt cache before installing pkgs 2020-11-05 11:47:21 +01:00
Jesse Duffield 5ff0ac2816 prevent crash when removing remote with no urls 2020-11-05 21:32:08 +11:00
Dawid Dziurla 7c1889cd70 Merge pull request #1051 from jesseduffield/go-1.10-compat
gui: fix go-1.10 compatibility
2020-10-14 12:53:29 +02:00
Dawid Dziurla 5669cc0002 gui: fix go-1.10 compatibility 2020-10-14 12:43:31 +02:00
Dawid Dziurla d2ea5dd8b7 workflows: don't sign commit 2020-10-14 00:01:19 +02:00
Dawid Dziurla e0381b5920 workflows: run CD on Ubuntu 20.04
So we can get newer git-buildpackage
2020-10-13 23:51:40 +02:00
Dawid Dziurla dac3978983 Merge pull request #1049 from jesseduffield/cd-update-ppa
workflows: update PPA repo as part of CD process
2020-10-13 23:33:19 +02:00
Dawid Dziurla 7074cc28b8 Merge pull request #1050 from jesseduffield/dawidd6-patch-1
utils: ReplaceAll -> Replace
2020-10-13 17:28:33 +02:00
Dawid Dziurla 327b6ad097 utils: ReplaceAll -> Replace
Fix compatibility with older Go compiler versions
2020-10-13 17:25:37 +02:00
Dawid Dziurla 1dc837527f workflows: update PPA repo as part of CD process 2020-10-13 13:43:14 +02:00
Jesse Duffield b1dd3c4866 support rebinding confirm/newline keys in editor 2020-10-13 08:21:09 +11:00
Jesse Duffield 624fb8da21 preserve width of side panel when main view split unless window is wide enough 2020-10-13 07:31:14 +11:00
nullawhale 1ff405edd8 Copy a commit message to clipboard: Changes to latest version 2020-10-12 21:04:01 +11:00
Jesse Duffield 031e97ef91 more password checks on commands that talk to the remote 2020-10-12 19:07:40 +11:00
119 changed files with 2277 additions and 839 deletions
+26 -2
View File
@@ -6,7 +6,7 @@ on:
- 'v*'
jobs:
cd:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -21,8 +21,32 @@ jobs:
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
homebrew:
runs-on: macos-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
ppa:
runs-on: ubuntu-20.04
steps:
- name: Checkout PPA repo
uses: actions/checkout@v2
with:
repository: dawidd6/lazygit-debian
token: ${{secrets.GITHUB_API_TOKEN}}
fetch-depth: 0
- name: Update PPA repo
run: |
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
sudo apt update
sudo apt install -y git-buildpackage
git fetch --tags https://github.com/$GITHUB_REPOSITORY
gbp import-ref -u "$version"
gbp dch -D xenial -N "$version"-1
git add debian/changelog
git commit -m "d/changelog: dch $version"
gbp tag
git push --tags origin master
+14
View File
@@ -0,0 +1,14 @@
name: Lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
+1
View File
@@ -31,3 +31,4 @@ test/integration/*/used_config/
# these sample hooks waste too space space
test/integration/*/expected/.git_keep/hooks/
!.git_keep/
lazygit.exe
+10 -1
View File
@@ -30,6 +30,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
- [Chocolatey (Windows)](#chocolatey-windows)
- [Manual](#manual)
- [Usage](#usage)
- [Keybindings](#keybindings)
@@ -52,7 +53,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
### Homebrew
@@ -166,6 +167,14 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
### Chocolatey (Windows)
You can install `lazygit` using [Chocolatey](https://chocolatey.org/):
```sh
choco install lazygit
```
### Manual
You'll need to [install Go](https://golang.org/doc/install)
+26
View File
@@ -48,6 +48,7 @@ Default path for the config file:
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
allBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
update:
@@ -58,6 +59,7 @@ Default path for the config file:
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: true
disableStartupPopups: false
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
keybinding:
universal:
quit: 'q'
@@ -111,6 +113,8 @@ Default path for the config file:
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<tab>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -156,6 +160,7 @@ Default path for the config file:
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
stash:
popStash: 'g'
commitFiles:
@@ -336,3 +341,24 @@ git:
Result:
![](https://i.imgur.com/Nibq35B.png)
## Launching not in a repository behaviour
By default, when launching lazygit from a directory that is not a repository,
you will be prompted to choose if you would like to initialize a repo. You can
override this behaviour in the config with one of the following:
```yaml
# for default prompting behaviour
notARepository: 'prompt'
```
```yaml
# to skip and initialize a new repo
notARepository: 'create'
```
```yaml
# to skip without creating a new repo
notARepository: 'skip'
```
+3 -3
View File
@@ -2,7 +2,7 @@
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
```
```yml
customCommands:
- key: '<c-r>'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
@@ -95,7 +95,7 @@ The permitted option fields are:
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```
```yml
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
@@ -123,7 +123,7 @@ SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions
+7
View File
@@ -0,0 +1,7 @@
# Documentation Overview
* [Configuration](./Config.md).
* [Custom Commands](./Custom_Command_Keybindings.md)
* [Custom Pagers](./Custom_Pagers.md)
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)
+2
View File
@@ -23,11 +23,13 @@ require (
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9
github.com/mgutz/str v1.2.0
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/sahilm/fuzzy v0.1.0
github.com/sirupsen/logrus v1.4.2
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
+4
View File
@@ -86,6 +86,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -114,6 +116,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+14 -4
View File
@@ -192,10 +192,20 @@ func (app *App) setupRepo() (bool, error) {
return false, err // Current directory appears to be a git repository.
}
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
shouldInitRepo = false
}
} else if notARepository == "skip" {
shouldInitRepo = false
}
if !shouldInitRepo {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
+7 -1
View File
@@ -22,7 +22,13 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options oscomman
// Commit commits to git
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
command := fmt.Sprintf("git commit %s -m %s", flags, strconv.Quote(message))
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
}
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
if c.usingGpg() {
return c.OSCommand.ShellCommandFromString(command), nil
}
+8 -5
View File
@@ -43,10 +43,13 @@ func (c *GitCommand) colorArg() string {
}
func (c *GitCommand) GetConfigValue(key string) string {
output, err := c.OSCommand.RunCommandWithOutput("git config --get %s", key)
if err != nil {
// looks like this returns an error if there is no matching value which we're okay with
return ""
value, _ := c.getLocalGitConfig(key)
// we get an error if the key doesn't exist which we don't care about
if value != "" {
return value
}
return strings.TrimSpace(output)
value, _ = c.getGlobalGitConfig(key)
return value
}
+27
View File
@@ -2,6 +2,7 @@ package commands
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
)
// CatFile obtains the content of a file
@@ -262,3 +264,28 @@ func (c *GitCommand) ResetAndClean() error {
return c.RemoveUntrackedFiles()
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.OSCommand.Getenv("VISUAL")
}
if editor == "" {
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
}
+230 -7
View File
@@ -954,15 +954,23 @@ func TestGitCommandAmendHead(t *testing.T) {
// TestGitCommandPush is a function.
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
forcePush bool
test func(error)
testName string
getLocalGitConfig func(string) (string, error)
getGlobalGitConfig func(string) (string, error)
command func(string, ...string) *exec.Cmd
forcePush bool
test func(error)
}
scenarios := []scenario{
{
"Push with force disabled",
"Push with force disabled, follow-tags on",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
@@ -975,7 +983,13 @@ func TestGitCommandPush(t *testing.T) {
},
},
{
"Push with force enabled",
"Push with force enabled, follow-tags on",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
@@ -988,7 +1002,51 @@ func TestGitCommandPush(t *testing.T) {
},
},
{
"Push with an error occurring",
"Push with force disabled, follow-tags off locally",
func(string) (string, error) {
return "false", nil
},
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return exec.Command("echo")
},
false,
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with force enabled, follow-tags off globally",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "false", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
return exec.Command("echo")
},
true,
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with an error occurring, follow-tags on",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
@@ -1005,6 +1063,8 @@ func TestGitCommandPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.getLocalGitConfig = s.getLocalGitConfig
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
return "\n"
})
@@ -1383,6 +1443,18 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
assert.NoError(t, err)
}
func TestGitCommandGetAllBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
return exec.Command("echo")
}
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
assert.NoError(t, err)
}
// TestGitCommandDiff is a function.
func TestGitCommandDiff(t *testing.T) {
type scenario struct {
@@ -2087,3 +2159,154 @@ func TestFindDotGitDir(t *testing.T) {
})
}
}
// TestEditFile is a function.
func TestEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "emacs", name)
return nil
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"file/with space",
func(name string, args ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
assert.EqualValues(t, "file/with space", args[0])
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.Getenv = s.getenv
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
s.test(gitCmd.EditFile(s.filename))
}
}
+2 -2
View File
@@ -185,9 +185,9 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
switch rebaseMode {
case "normal":
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case "interactive":
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
default:
return nil, nil
+1 -3
View File
@@ -1,7 +1,6 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
@@ -26,8 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
return c.getUnfilteredStashEntries()
}
unescaped := fmt.Sprintf("git stash list --name-only")
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
rawString, err := c.OSCommand.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}
+15 -42
View File
@@ -19,7 +19,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
@@ -36,25 +35,23 @@ type Platform struct {
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
GetGlobalGitConfig func(string) (string, error)
Getenv func(string) string
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
Getenv func(string) string
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
Command: exec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
GetGlobalGitConfig: gitconfig.Global,
Getenv: os.Getenv,
Log: log,
Platform: getPlatform(),
Config: config,
Command: exec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
Getenv: os.Getenv,
}
}
@@ -235,31 +232,6 @@ func (c *OSCommand) OpenLink(link string) error {
return err
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.GetGlobalGitConfig("core.editor")
if editor == "" {
editor = c.Getenv("VISUAL")
}
if editor == "" {
editor = c.Getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
editor = "vi"
}
}
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.Quote(filename)))
return c.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
@@ -471,12 +443,13 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
return err
}
if stop {
cmd.Process.Kill()
_ = cmd.Process.Kill()
break
}
}
cmd.Wait()
_ = cmd.Wait()
return nil
}
-152
View File
@@ -109,158 +109,6 @@ func TestOSCommandOpenFile(t *testing.T) {
}
}
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "emacs", name)
return nil
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"file/with space",
func(name string, args ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
assert.EqualValues(t, "file/with space", args[0])
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.GetGlobalGitConfig = s.getGlobalGitConfig
OSCmd.Getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
+2 -2
View File
@@ -149,7 +149,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == "rebasing" {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -169,7 +169,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == "rebasing" {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
+25 -5
View File
@@ -91,10 +91,29 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *models.Branch) error {
pullRequestURL, err := pr.getPullRequestURL(branch)
if err != nil {
return err
}
return pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
}
// CopyURL copies the pull request URL to the clipboard
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
pullRequestURL, err := pr.getPullRequestURL(branch)
if err != nil {
return err
}
return pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
}
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
}
repoURL := pr.GitCommand.GetRemoteURL()
@@ -108,14 +127,15 @@ func (pr *PullRequest) Create(branch *models.Branch) error {
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.UnsupportedGitService)
return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
}
repoInfo := getRepoInfoFromURL(repoURL)
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
pullRequestURL := fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
))
)
return pullRequestURL, nil
}
func getRepoInfoFromURL(url string) *RepoInformation {
+39 -25
View File
@@ -46,19 +46,21 @@ func TestGetRepoInfoFromURL(t *testing.T) {
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *models.Branch
command func(string, ...string) *exec.Cmd
test func(err error)
testName string
branch *models.Branch
remoteUrl string
command func(string, ...string) *exec.Cmd
test func(err error)
}
scenarios := []scenario{
{
"Opens a link to new pull request on bitbucket",
&models.Branch{
testName: "Opens a link to new pull request on bitbucket",
branch: &models.Branch{
Name: "feature/profile-page",
},
func(cmd string, args ...string) *exec.Cmd {
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
@@ -68,16 +70,17 @@ func TestCreatePullRequest(t *testing.T) {
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
return exec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on bitbucket with http remote url",
&models.Branch{
testName: "Opens a link to new pull request on bitbucket with http remote url",
branch: &models.Branch{
Name: "feature/events",
},
func(cmd string, args ...string) *exec.Cmd {
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
@@ -87,16 +90,17 @@ func TestCreatePullRequest(t *testing.T) {
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
return exec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on github",
&models.Branch{
testName: "Opens a link to new pull request on github",
branch: &models.Branch{
Name: "feature/sum-operation",
},
func(cmd string, args ...string) *exec.Cmd {
remoteUrl: "git@github.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@github.com:peter/calculator.git")
@@ -106,16 +110,17 @@ func TestCreatePullRequest(t *testing.T) {
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return exec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on gitlab",
&models.Branch{
testName: "Opens a link to new pull request on gitlab",
branch: &models.Branch{
Name: "feature/ui",
},
func(cmd string, args ...string) *exec.Cmd {
remoteUrl: "git@gitlab.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
@@ -125,19 +130,20 @@ func TestCreatePullRequest(t *testing.T) {
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return exec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.NoError(t, err)
},
},
{
"Throws an error if git service is unsupported",
&models.Branch{
testName: "Throws an error if git service is unsupported",
branch: &models.Branch{
Name: "feature/divide-operation",
},
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "git@something.com:peter/calculator.git")
remoteUrl: "git@something.com:peter/calculator.git",
command: func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.Error(t, err)
},
},
@@ -155,6 +161,14 @@ func TestCreatePullRequest(t *testing.T) {
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.getLocalGitConfig = func(path string) (string, error) {
assert.Equal(t, path, "remote.origin.url")
return s.remoteUrl, nil
}
gitCommand.getGlobalGitConfig = func(path string) (string, error) {
assert.Equal(t, path, "remote.origin.url")
return "", nil
}
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
+5 -2
View File
@@ -1,6 +1,8 @@
package commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -20,8 +22,9 @@ func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string) error {
return c.OSCommand.RunCommand("git push %s --delete %s", remoteName, branchName)
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}
// CheckRemoteBranchExists Returns remote branch
+12 -5
View File
@@ -6,6 +6,13 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
)
const (
REBASE_MODE_NORMAL = "normal"
REBASE_MODE_INTERACTIVE = "interactive"
REBASE_MODE_REBASING = "rebasing"
REBASE_MODE_MERGING = "merging"
)
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
@@ -14,11 +21,11 @@ func (c *GitCommand) RebaseMode() (string, error) {
return "", err
}
if exists {
return "normal", nil
return REBASE_MODE_NORMAL, nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
if exists {
return "interactive", err
return REBASE_MODE_INTERACTIVE, err
} else {
return "", err
}
@@ -27,13 +34,13 @@ func (c *GitCommand) RebaseMode() (string, error) {
func (c *GitCommand) WorkingTreeState() string {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return "rebasing"
return REBASE_MODE_REBASING
}
merging, _ := c.IsInMergeState()
if merging {
return "merging"
return REBASE_MODE_MERGING
}
return "normal"
return REBASE_MODE_NORMAL
}
// IsInMergeState states whether we are still mid-merge
+7 -5
View File
@@ -13,10 +13,7 @@ func (c *GitCommand) usingGpg() bool {
return false
}
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
}
gpgsign := c.GetConfigValue("commit.gpgsign")
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
@@ -24,6 +21,11 @@ func (c *GitCommand) usingGpg() bool {
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
followTagsFlag := "--follow-tags"
if c.GetConfigValue("push.followTags") == "false" {
followTagsFlag = ""
}
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
@@ -34,7 +36,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, args s
setUpstreamArg = "--set-upstream " + upstream
}
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
}
+5 -2
View File
@@ -1,5 +1,7 @@
package commands
import "fmt"
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
}
@@ -8,6 +10,7 @@ func (c *GitCommand) DeleteTag(tagName string) error {
return c.OSCommand.RunCommand("git tag -d %s", tagName)
}
func (c *GitCommand) PushTag(remoteName string, tagName string) error {
return c.OSCommand.RunCommand("git push %s %s", remoteName, tagName)
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}
+10 -5
View File
@@ -82,20 +82,25 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
}
func ConfigDir() string {
legacyConfigDirectory := configDirForVendor("jesseduffield")
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
return legacyConfigDirectory
}
configDirectory := configDirForVendor("")
return configDirectory
}
func configDirForVendor(vendor string) string {
envConfigDir := os.Getenv("CONFIG_DIR")
if envConfigDir != "" {
return envConfigDir
}
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := xdg.New("jesseduffield", "lazygit")
configDirs := xdg.New(vendor, "lazygit")
return configDirs.ConfigHome()
}
func findOrCreateConfigDir() (string, error) {
folder := ConfigDir()
err := os.MkdirAll(folder, 0755)
if err != nil {
return "", err
+68 -52
View File
@@ -14,19 +14,21 @@ type UserConfig struct {
DisableStartupPopups bool `yaml:"disableStartupPopups"`
CustomCommands []CustomCommand `yaml:"customCommands"`
Services map[string]string `yaml:"services"`
NotARepository string `yaml:"notARepository"`
}
type GuiConfig struct {
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
}
type ThemeConfig struct {
@@ -49,6 +51,7 @@ type GitConfig struct {
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
BranchLogCmd string `yaml:"branchLogCmd"`
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
OverrideGpg bool `yaml:"overrideGpg"`
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
@@ -144,11 +147,14 @@ type KeybindingUniversalConfig struct {
DiffingMenu string `yaml:"diffingMenu"`
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
CopyToClipboard string `yaml:"copyToClipboard"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
}
type KeybindingStatusConfig struct {
CheckForUpdate string `yaml:"checkForUpdate"`
RecentRepos string `yaml:"recentRepos"`
CheckForUpdate string `yaml:"checkForUpdate"`
RecentRepos string `yaml:"recentRepos"`
AllBranchesLogGraph string `yaml:"allBranchesLogGraph"`
}
type KeybindingFilesConfig struct {
@@ -167,6 +173,7 @@ type KeybindingFilesConfig struct {
type KeybindingBranchesConfig struct {
CreatePullRequest string `yaml:"createPullRequest"`
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
RebaseBranch string `yaml:"rebaseBranch"`
@@ -180,24 +187,25 @@ type KeybindingBranchesConfig struct {
}
type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
ViewResetOptions string `yaml:"viewResetOptions"`
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
CreateFixupCommit string `yaml:"createFixupCommit"`
SquashAboveCommits string `yaml:"squashAboveCommits"`
MoveDownCommit string `yaml:"moveDownCommit"`
MoveUpCommit string `yaml:"moveUpCommit"`
AmendToCommit string `yaml:"amendToCommit"`
PickCommit string `yaml:"pickCommit"`
RevertCommit string `yaml:"revertCommit"`
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
ViewResetOptions string `yaml:"viewResetOptions"`
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
CreateFixupCommit string `yaml:"createFixupCommit"`
SquashAboveCommits string `yaml:"squashAboveCommits"`
MoveDownCommit string `yaml:"moveDownCommit"`
MoveUpCommit string `yaml:"moveUpCommit"`
AmendToCommit string `yaml:"amendToCommit"`
PickCommit string `yaml:"pickCommit"`
RevertCommit string `yaml:"revertCommit"`
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
}
type KeybindingStashConfig struct {
@@ -276,7 +284,8 @@ func GetDefaultConfig() *UserConfig {
SelectedLineBgColor: []string{"default"},
SelectedRangeBgColor: []string{"blue"},
},
CommitLength: CommitLengthConfig{Show: true},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,
},
Git: GitConfig{
Paging: PagingConfig{
@@ -293,7 +302,7 @@ func GetDefaultConfig() *UserConfig {
SkipHookPrefix: "WIP",
AutoFetch: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
OverrideGpg: false,
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
DisableForcePushing: false,
CommitPrefixes: map[string]CommitPrefixConfig(nil),
},
@@ -359,10 +368,13 @@ func GetDefaultConfig() *UserConfig {
DiffingMenu: "W",
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<tab>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
RecentRepos: "<enter>",
CheckForUpdate: "u",
RecentRepos: "<enter>",
AllBranchesLogGraph: "a",
},
Files: KeybindingFilesConfig{
CommitChanges: "c",
@@ -378,6 +390,7 @@ func GetDefaultConfig() *UserConfig {
Fetch: "f",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
CreatePullRequest: "o",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
@@ -390,24 +403,26 @@ func GetDefaultConfig() *UserConfig {
SetUpstream: "u",
FetchRemote: "f",
},
Commits: KeybindingCommitsConfig{SquashDown: "s",
RenameCommit: "r",
RenameCommitWithEditor: "R",
ViewResetOptions: "g",
MarkCommitAsFixup: "f",
CreateFixupCommit: "F",
SquashAboveCommits: "S",
MoveDownCommit: "<c-j>",
MoveUpCommit: "<c-k>",
AmendToCommit: "A",
PickCommit: "p",
RevertCommit: "t",
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
Commits: KeybindingCommitsConfig{
SquashDown: "s",
RenameCommit: "r",
RenameCommitWithEditor: "R",
ViewResetOptions: "g",
MarkCommitAsFixup: "f",
CreateFixupCommit: "F",
SquashAboveCommits: "S",
MoveDownCommit: "<c-j>",
MoveUpCommit: "<c-k>",
AmendToCommit: "A",
PickCommit: "p",
RevertCommit: "t",
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitMessageToClipboard: "<c-y>",
},
Stash: KeybindingStashConfig{
PopStash: "g",
@@ -431,5 +446,6 @@ func GetDefaultConfig() *UserConfig {
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
NotARepository: "prompt",
}
}
+71 -24
View File
@@ -1,6 +1,7 @@
package gui
import (
"sync"
"time"
"github.com/jesseduffield/gocui"
@@ -8,33 +9,68 @@ import (
)
type appStatus struct {
name string
message string
statusType string
duration int
id int
}
type statusManager struct {
statuses []appStatus
nextId int
mutex sync.Mutex
}
func (m *statusManager) removeStatus(name string) {
func (m *statusManager) removeStatus(id int) {
m.mutex.Lock()
defer m.mutex.Unlock()
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.name != name {
if status.id != id {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
}
func (m *statusManager) addWaitingStatus(name string) {
m.removeStatus(name)
func (m *statusManager) addWaitingStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId += 1
id := m.nextId
newStatus := appStatus{
name: name,
message: message,
statusType: "waiting",
duration: 0,
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
return id
}
func (m *statusManager) addToastStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId++
id := m.nextId
newStatus := appStatus{
message: message,
statusType: "toast",
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
go func() {
time.Sleep(time.Second * 2)
m.removeStatus(id)
}()
return id
}
func (m *statusManager) getStatusString() string {
@@ -43,31 +79,42 @@ func (m *statusManager) getStatusString() string {
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader()
return topStatus.message + " " + utils.Loader()
}
return topStatus.name
return topStatus.message
}
func (gui *Gui) raiseToast(message string) {
gui.statusManager.addToastStatus(message)
gui.renderAppStatus()
}
func (gui *Gui) renderAppStatus() {
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
gui.renderString("appStatus", "")
return
}
gui.renderString("appStatus", appStatus)
}
})
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
go utils.Safe(func() {
gui.statusManager.addWaitingStatus(name)
id := gui.statusManager.addWaitingStatus(message)
defer func() {
gui.statusManager.removeStatus(name)
gui.statusManager.removeStatus(id)
}()
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
return
}
gui.renderString("appStatus", appStatus)
}
})
gui.renderAppStatus()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
+29 -18
View File
@@ -47,7 +47,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
if gui.isMainPanelSplit() {
if gui.splitMainPanelSideBySide() {
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
}
@@ -108,6 +108,28 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*
return result
}
func (gui *Gui) splitMainPanelSideBySide() bool {
if !gui.isMainPanelSplit() {
return false
}
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
case "vertical":
return false
case "horizontal":
return true
default:
if width < 200 && height > 30 { // 2 80 character width panels + 40 width for side panel
return false
} else {
return true
}
}
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
@@ -119,6 +141,11 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
@@ -132,23 +159,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
ConditionalChildren: gui.sidePanelChildren,
},
{
ConditionalDirection: func(width int, height int) int {
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
switch mainPanelSplitMode {
case "vertical":
return boxlayout.ROW
case "horizontal":
return boxlayout.COLUMN
default:
if width < 160 && height > 30 { // 2 80 character width panels
return boxlayout.ROW
} else {
return boxlayout.COLUMN
}
}
},
Direction: boxlayout.COLUMN,
Direction: mainPanelsDirection,
Weight: mainSectionWeight,
Children: gui.mainSectionChildren(),
},
+1
View File
@@ -182,6 +182,7 @@ func TestArrangeWindows(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
})
+93 -45
View File
@@ -7,6 +7,8 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -99,8 +101,21 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
return nil
}
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
if err := pullRequest.CopyURL(branch); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(v, gui.Tr.FetchWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
return err
}
go utils.Safe(func() {
@@ -194,21 +209,24 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.BranchName+":", "", func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.findBranchNameSuggestions,
handleConfirm: func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.ask(askOpts{
title: gui.Tr.BranchNotFoundTitle,
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
})
return gui.ask(askOpts{
title: gui.Tr.BranchNotFoundTitle,
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
}},
)
}
func (gui *Gui) getCheckedOutBranch() *models.Branch {
@@ -265,7 +283,6 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
)
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
@@ -302,7 +319,6 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
)
return gui.ask(askOpts{
title: gui.Tr.MergingTitle,
prompt: prompt,
handleConfirm: func() error {
@@ -344,7 +360,6 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
)
return gui.ask(askOpts{
title: gui.Tr.RebasingTitle,
prompt: prompt,
handleConfirm: func() error {
@@ -386,7 +401,7 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
},
)
go utils.Safe(func() {
_ = gui.createLoaderPanel(v, message)
_ = gui.createLoaderPanel(message)
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
@@ -418,17 +433,20 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
// way to get it to show up in the reflog)
promptForNewName := func() error {
return gui.prompt(gui.Tr.NewBranchNamePrompt+" "+branch.Name+":", "", func(newBranchName string) error {
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
return gui.surfaceError(err)
}
// need to checkout so that the branch shows up in our reflog and therefore
// doesn't get lost among all the other branches when we switch to something else
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
return gui.prompt(promptOpts{
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
handleConfirm: func(newBranchName string) error {
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
return gui.surfaceError(err)
}
// need to checkout so that the branch shows up in our reflog and therefore
// doesn't get lost among all the other branches when we switch to something else
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
@@ -441,7 +459,6 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
}
return gui.ask(askOpts{
title: gui.Tr.LcRenameBranch,
prompt: gui.Tr.RenameBranchWarning,
handleConfirm: promptForNewName,
@@ -475,25 +492,56 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
// will set to the remote's existing name
prefilledName = item.ID()
}
return gui.prompt(message, prefilledName, func(response string) error {
if err := gui.GitCommand.NewBranch(response, item.ID()); err != nil {
return err
}
// if we're currently in the branch commits context then the selected commit
// is about to go to the top of the list
if context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
context.GetPanelState().SetSelectedLineIdx(0)
}
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
if err := gui.switchContext(gui.Contexts.Branches.Context); err != nil {
return gui.prompt(promptOpts{
title: message,
initialContent: prefilledName,
handleConfirm: func(response string) error {
if err := gui.GitCommand.NewBranch(response, item.ID()); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLineIdx = 0
// if we're currently in the branch commits context then the selected commit
// is about to go to the top of the list
if context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
context.GetPanelState().SetSelectedLineIdx(0)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
if err := gui.pushContext(gui.Contexts.Branches.Context); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLineIdx = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
func (gui *Gui) getBranchNames() []string {
result := make([]string, len(gui.State.Branches))
for i, branch := range gui.State.Branches {
result[i] = branch.Name
}
return result
}
func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
branchNames := gui.getBranchNames()
matchingBranchNames := utils.FuzzySearch(input, branchNames)
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
for i, branchName := range matchingBranchNames {
suggestions[i] = &types.Suggestion{
Value: branchName,
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
}
}
return suggestions
}
+3 -3
View File
@@ -176,7 +176,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
}
}
if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); err != nil {
if err := gui.pushContext(gui.Contexts.PatchBuilding.Context); err != nil {
return err
}
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
@@ -192,7 +192,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
},
})
}
@@ -215,5 +215,5 @@ func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, conte
return err
}
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
}
-35
View File
@@ -79,38 +79,3 @@ func (gui *Gui) RenderCommitLength() {
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
+37 -23
View File
@@ -233,12 +233,16 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
return gui.surfaceError(err)
}
return gui.prompt(gui.Tr.LcRenameCommit, message, func(response string) error {
if err := gui.GitCommand.RenameCommit(response); err != nil {
return gui.surfaceError(err)
}
return gui.prompt(promptOpts{
title: gui.Tr.LcRenameCommit,
initialContent: message,
handleConfirm: func(response string) error {
if err := gui.GitCommand.RenameCommit(response); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
}
@@ -445,19 +449,6 @@ func (gui *Gui) handleViewCommitFiles() error {
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
}
func (gui *Gui) hasCommit(commits []*models.Commit, target string) (int, bool) {
for idx, commit := range commits {
if commit.Sha == target {
return idx, true
}
}
return -1, false
}
func (gui *Gui) unchooseCommit(commits []*models.Commit, i int) []*models.Commit {
return append(commits[:i], commits[i+1:]...)
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
@@ -530,11 +521,14 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.prompt(gui.Tr.TagNameTitle, "", func(response string) error {
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
return gui.prompt(promptOpts{
title: gui.Tr.TagNameTitle,
handleConfirm: func(response string) error {
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
},
})
}
@@ -591,3 +585,23 @@ func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) err
return nil
}
func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
commit := gui.getSelectedLocalCommit()
if commit == nil {
return nil
}
message, err := gui.GitCommand.GetCommitMessage(commit.Sha)
if err != nil {
return gui.surfaceError(err)
}
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
return nil
}
+111 -27
View File
@@ -12,6 +12,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -27,6 +28,8 @@ type createPopupPanelOpts struct {
// when handlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context.
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
type askOpts struct {
@@ -35,13 +38,14 @@ type askOpts struct {
handleConfirm func() error
handleClose func() error
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
func (gui *Gui) createLoaderPanel(currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
type promptOpts struct {
title string
initialContent string
handleConfirm func(string) error
findSuggestionsFunc func(string) []*types.Suggestion
}
func (gui *Gui) ask(opts askOpts) error {
@@ -51,20 +55,29 @@ func (gui *Gui) ask(opts askOpts) error {
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
func (gui *Gui) prompt(title string, initialContent string, handleConfirm func(string) error) error {
func (gui *Gui) prompt(opts promptOpts) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: title,
prompt: initialContent,
title: opts.title,
prompt: opts.initialContent,
editable: true,
handleConfirmPrompt: handleConfirm,
handleConfirmPrompt: opts.handleConfirm,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) createLoaderPanel(prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error {
return func() error {
if function != nil {
if err := function(); err != nil {
return err
@@ -79,10 +92,10 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f
}
}
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error, getResponse func() string) func() error {
return func() error {
if function != nil {
if err := function(v.Buffer()); err != nil {
if err := function(getResponse()); err != nil {
return gui.surfaceError(err)
}
}
@@ -117,6 +130,9 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
}
gui.deleteConfirmationView()
_, _ = gui.g.SetViewOnBottom("suggestions")
return nil
}
@@ -156,11 +172,11 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (*gocui.View, error) {
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
confirmationView.HasLoader = hasLoader
@@ -171,8 +187,24 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
}
gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil {
suggestionsViewHeight := 11
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
suggestionsView.Wrap = true
suggestionsView.FgColor = theme.GocuiDefaultTextColor
}
gui.setSuggestions([]*types.Suggestion{})
_, _ = gui.g.SetViewOnTop("suggestions")
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchContext(gui.Contexts.Confirmation.Context)
return gui.pushContext(gui.Contexts.Confirmation.Context)
})
return confirmationView, nil
}
@@ -183,11 +215,12 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
if view, _ := g.View("confirmation"); view != nil {
gui.deleteConfirmationView()
}
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader)
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
if err != nil {
return err
}
confirmationView.Editable = opts.editable
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.editable {
go utils.Safe(func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
@@ -200,6 +233,7 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
}
gui.renderString("confirmation", opts.prompt)
return gui.setKeyBindings(opts)
})
return nil
@@ -215,22 +249,72 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
)
gui.renderString("options", actions)
var onConfirm func(*gocui.Gui, *gocui.View) error
var onConfirm func() error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt)
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getConfirmationView().Buffer() })
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone, onConfirm); err != nil {
return err
}
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone, onConfirm); err != nil {
return err
type confirmationKeybinding struct {
viewName string
key interface{}
handler func() error
}
return gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
keybindingConfig := gui.Config.GetUserConfig().Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
confirmationKeybindings := []confirmationKeybinding{
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.Confirm),
handler: onConfirm,
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
handler: onConfirm,
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.Return),
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Suggestions.Context) },
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.Confirm),
handler: onSuggestionConfirm,
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
handler: onSuggestionConfirm,
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.Return),
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Confirmation.Context) },
},
}
for _, binding := range confirmationKeybindings {
if err := gui.g.SetKeybinding(binding.viewName, nil, binding.key, gocui.ModNone, gui.wrappedHandler(binding.handler)); err != nil {
return err
}
}
return nil
}
func (gui *Gui) createErrorPanel(message string) error {
+49 -25
View File
@@ -35,6 +35,7 @@ const (
SEARCH_CONTEXT_KEY = "search"
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
SUBMODULES_CONTEXT_KEY = "submodules"
SUGGESTIONS_CONTEXT_KEY = "suggestions"
)
var allContextKeys = []string{
@@ -59,6 +60,7 @@ var allContextKeys = []string{
SEARCH_CONTEXT_KEY,
COMMIT_MESSAGE_CONTEXT_KEY,
SUBMODULES_CONTEXT_KEY,
SUGGESTIONS_CONTEXT_KEY,
}
type SimpleContextNode struct {
@@ -91,6 +93,7 @@ type ContextTree struct {
Confirmation SimpleContextNode
CommitMessage SimpleContextNode
Search SimpleContextNode
Suggestions SimpleContextNode
}
func (gui *Gui) allContexts() []Context {
@@ -115,6 +118,7 @@ func (gui *Gui) allContexts() []Context {
gui.Contexts.Merging.Context,
gui.Contexts.PatchBuilding.Context,
gui.Contexts.SubCommits.Context,
gui.Contexts.Suggestions.Context,
}
}
@@ -280,9 +284,7 @@ func (gui *Gui) contextTree() ContextTree {
},
Merging: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return gui.refreshMergePanel()
},
OnFocus: gui.refreshMergePanel,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
@@ -291,7 +293,7 @@ func (gui *Gui) contextTree() ContextTree {
},
Credentials: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCredentialsViewFocused() },
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
@@ -305,9 +307,12 @@ func (gui *Gui) contextTree() ContextTree {
Key: CONFIRMATION_CONTEXT_KEY,
},
},
Suggestions: SimpleContextNode{
Context: gui.suggestionsListContext(),
},
CommitMessage: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCommitMessageFocused() },
OnFocus: gui.handleCommitMessageFocused,
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
@@ -402,7 +407,24 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
return ""
}
func (gui *Gui) switchContext(c Context) error {
// use replaceContext when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
if len(gui.State.ContextStack) == 0 {
gui.State.ContextStack = []Context{c}
} else {
// replace the last item with the given item
gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c)
}
return gui.activateContext(c)
})
return nil
}
func (gui *Gui) pushContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
// push onto stack
// if we are switching to a side context, remove all other contexts in the stack
@@ -426,11 +448,11 @@ func (gui *Gui) switchContext(c Context) error {
return nil
}
// switchContextToView is to be used when you don't know which context you
// pushContextWithView is to be used when you don't know which context you
// want to switch to: you only know the view that you want to switch to. It will
// look up the context currently active for that view and switch to that context
func (gui *Gui) switchContextToView(viewName string) error {
return gui.switchContext(gui.State.ViewContextMap[viewName])
func (gui *Gui) pushContextWithView(viewName string) error {
return gui.pushContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) returnFromContext() error {
@@ -513,7 +535,7 @@ func (gui *Gui) activateContext(c Context) error {
if viewName == "main" {
gui.changeMainViewsContext(c.GetKey())
} else {
gui.changeMainViewsContext("normal")
gui.changeMainViewsContext(MAIN_NORMAL_CONTEXT_KEY)
}
gui.setViewTabForContext(c)
@@ -556,13 +578,14 @@ func (gui *Gui) activateContext(c Context) error {
return nil
}
func (gui *Gui) renderContextStack() string {
result := ""
for _, context := range gui.State.ContextStack {
result += context.GetKey() + "\n"
}
return result
}
// currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextStack {
// result += context.GetKey() + "\n"
// }
// return result
// }
func (gui *Gui) currentContext() Context {
if len(gui.State.ContextStack) == 0 {
@@ -755,16 +778,17 @@ func (gui *Gui) rerenderView(viewName string) error {
return context.HandleRender()
}
func (gui *Gui) getCurrentSideView() *gocui.View {
currentSideContext := gui.currentSideContext()
if currentSideContext == nil {
return nil
}
// currently unused
// func (gui *Gui) getCurrentSideView() *gocui.View {
// currentSideContext := gui.currentSideContext()
// if currentSideContext == nil {
// return nil
// }
view, _ := gui.g.View(currentSideContext.GetViewName())
// view, _ := gui.g.View(currentSideContext.GetViewName())
return view
}
// return view
// }
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideContext()
+6 -5
View File
@@ -14,18 +14,19 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
gui.credentials = make(chan string)
gui.g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
switch passOrUname {
case "username":
credentialsView.Title = gui.Tr.CredentialsUsername
credentialsView.Mask = 0
} else if passOrUname == "password" {
case "password":
credentialsView.Title = gui.Tr.CredentialsPassword
credentialsView.Mask = '*'
} else {
default:
credentialsView.Title = gui.Tr.CredentialsPassphrase
credentialsView.Mask = '*'
}
if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Credentials.Context); err != nil {
return err
}
@@ -77,7 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
errMessage = gui.Tr.PassUnameWrong
}
// we are not logging this error because it may contain a password or a passphrase
gui.createErrorPanel(errMessage)
_ = gui.createErrorPanel(errMessage)
} else {
_ = gui.closeConfirmationPrompt(false)
}
+5 -5
View File
@@ -98,15 +98,15 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
return gui.surfaceError(err)
}
return gui.prompt(
title,
initialValue,
func(str string) error {
return gui.prompt(promptOpts{
title: title,
initialContent: initialValue,
handleConfirm: func(str string) error {
promptResponses[idx] = str
return wrappedF()
},
)
})
}
case "menu":
f = func() error {
+6 -3
View File
@@ -128,9 +128,12 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
{
displayString: gui.Tr.LcEnterRefToDiff,
onPress: func() error {
return gui.prompt(gui.Tr.LcEnteRefName, "", func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.prompt(promptOpts{
title: gui.Tr.LcEnteRefName,
handleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
},
},
+1 -12
View File
@@ -2,19 +2,8 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (gui *Gui) submoduleFromFile(file *models.File) *models.SubmoduleConfig {
for _, config := range gui.State.Submodules {
if config.Name == file.Name {
return config
}
}
return nil
}
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
@@ -31,7 +20,7 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
{
displayString: gui.Tr.LcSubmoduleStashAndReset,
onPress: func() error {
return gui.resetSubmodule(submodule)
return gui.handleResetSubmodule(submodule)
},
},
}
+80
View File
@@ -0,0 +1,80 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyTab
}
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == newlineKey:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
if gui.findSuggestions != nil {
input := v.Buffer()
suggestions := gui.findSuggestions(input)
gui.setSuggestions(suggestions)
}
}
+1 -16
View File
@@ -28,21 +28,6 @@ func NewFileWatcher(log *logrus.Entry) *fileWatcher {
return &fileWatcher{
Disabled: true,
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error(err)
return &fileWatcher{
Disabled: true,
}
}
return &fileWatcher{
Watcher: watcher,
Log: log,
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
}
}
func (w *fileWatcher) watchingFilename(filename string) bool {
@@ -132,7 +117,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
}
// watch for errors
+56 -33
View File
@@ -152,7 +152,7 @@ func (gui *Gui) trackedFiles() []*models.File {
return result
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
func (gui *Gui) stageSelectedFile() error {
file := gui.getSelectedFile()
if file == nil {
return nil
@@ -183,7 +183,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
gui.switchContext(gui.Contexts.Staging.Context)
_ = gui.pushContext(gui.Contexts.Staging.Context)
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
}
@@ -284,7 +284,7 @@ func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
gui.renderStringSync("commitMessage", skipHookPreifx)
_ = gui.renderStringSync("commitMessage", skipHookPreifx)
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
@@ -301,11 +301,27 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
return &cfg
}
func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
err := gui.GitCommand.StageAll()
if err != nil {
return err
}
return gui.refreshFilesAndSubmodules()
}
return nil
}
func (gui *Gui) handleCommitPress() error {
if err := gui.prepareFilesForCommit(); err != nil {
return gui.surfaceError(err)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitPress()
})
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
}
commitMessageView := gui.getCommitMessageView()
@@ -325,7 +341,7 @@ func (gui *Gui) handleCommitPress() error {
}
gui.g.Update(func(g *gocui.Gui) error {
if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
if err := gui.pushContext(gui.Contexts.CommitMessage.Context); err != nil {
return err
}
@@ -354,9 +370,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
func (gui *Gui) handleAmendCommitPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleAmendCommitPress()
})
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
}
if len(gui.State.Commits) == 0 {
@@ -386,9 +400,7 @@ func (gui *Gui) handleAmendCommitPress() error {
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitEditorPress()
})
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
}
gui.PrepareSubProcess("git commit")
@@ -405,7 +417,7 @@ func (gui *Gui) PrepareSubProcess(command string) {
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
_, err := gui.runSyncOrAsyncCommand(gui.GitCommand.EditFile(filename))
return err
}
@@ -484,15 +496,19 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
}
}
return gui.prompt(gui.Tr.EnterUpstream, "origin/"+currentBranch.Name, func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
handleConfirm: func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(errorMessage)
}
return gui.createErrorPanel(errorMessage)
}
return gui.pullFiles(PullFilesOptions{})
return gui.pullFiles(PullFilesOptions{})
},
})
}
@@ -505,13 +521,13 @@ type PullFilesOptions struct {
}
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
if err := gui.createLoaderPanel(gui.g.CurrentView(), gui.Tr.PullWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.PullWait); err != nil {
return err
}
mode := gui.Config.GetUserConfig().Git.Pull.Mode
go utils.Safe(func() { gui.pullWithMode(mode, opts) })
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
return nil
}
@@ -548,7 +564,7 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
}
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(v, gui.Tr.PushWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
return err
}
go utils.Safe(func() {
@@ -557,10 +573,10 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
if forcePushDisabled {
gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
return
}
gui.ask(askOpts{
_ = gui.ask(askOpts{
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
@@ -598,8 +614,12 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
} else {
return gui.prompt(gui.Tr.EnterUpstream, "origin "+currentBranch.Name, func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
handleConfirm: func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
},
})
}
} else if currentBranch.Pullables == "0" {
@@ -630,7 +650,7 @@ func (gui *Gui) handleSwitchToMerge() error {
return gui.createErrorPanel(gui.Tr.FileNoMergeCons)
}
return gui.switchContext(gui.Contexts.Merging.Context)
return gui.pushContext(gui.Contexts.Merging.Context)
}
func (gui *Gui) openFile(filename string) error {
@@ -650,9 +670,12 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.CustomCommand, "", func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
handleConfirm: func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
},
})
}
+3 -5
View File
@@ -3,11 +3,9 @@ package gui
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.State.Modes.Filtering.Active() {
err := gui.ask(askOpts{
title: gui.Tr.MustExitFilterModeTitle,
prompt: gui.Tr.MustExitFilterModePrompt,
handleConfirm: func() error {
return gui.exitFilterMode()
},
title: gui.Tr.MustExitFilterModeTitle,
prompt: gui.Tr.MustExitFilterModePrompt,
handleConfirm: gui.exitFilterMode,
})
return false, err
+6 -3
View File
@@ -41,9 +41,12 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcFilterPathOption,
onPress: func() error {
return gui.prompt(gui.Tr.LcEnterFileName, "", func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
return gui.prompt(promptOpts{
title: gui.Tr.LcEnterFileName,
handleConfirm: func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
},
})
},
})
+8 -4
View File
@@ -52,10 +52,14 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
startHandler := func(branchType string) func() error {
return func() error {
title := utils.ResolvePlaceholderString(gui.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType})
return gui.prompt(title, "", func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
return gui.prompt(promptOpts{
title: title,
handleConfirm: func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
},
})
}
}
+12 -3
View File
@@ -1,6 +1,7 @@
package gui
import (
"fmt"
"math"
"strings"
@@ -175,10 +176,10 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
err = gui.GitCommand.Fetch(fetchOpts)
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
gui.createErrorPanel(gui.Tr.PassUnameWrong)
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
}
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
_ = gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
return err
}
@@ -191,5 +192,13 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil
}
return gui.OSCommand.CopyToClipboard(itemId)
if err := gui.OSCommand.CopyToClipboard(itemId); err != nil {
return gui.surfaceError(err)
}
truncatedItemId := utils.TruncateWithEllipsis(strings.Replace(itemId, "\n", " ", -1), 50)
gui.raiseToast(fmt.Sprintf("'%s' %s", truncatedItemId, gui.Tr.LcCopiedToClipboard))
return nil
}
+16
View File
@@ -21,6 +21,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
@@ -51,6 +52,8 @@ type SentinelErrors struct {
ErrRestart error
}
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
// because we can't do package-scoped errors with localization, and also because
// it seems like package-scoped variables are bad in general
@@ -110,6 +113,10 @@ type Gui struct {
StartTime time.Time
Mutexes guiStateMutexes
// findSuggestions will take a string that the user has typed into a prompt
// and return a slice of suggestions which match that string.
findSuggestions func(string) []*types.Suggestion
}
type RecordedEvent struct {
@@ -218,6 +225,10 @@ type submodulePanelState struct {
listPanelState
}
type suggestionsPanelState struct {
listPanelState
}
type panelStates struct {
Files *filePanelState
Branches *branchPanelState
@@ -233,6 +244,7 @@ type panelStates struct {
Merging *mergingPanelState
CommitFiles *commitFilesPanelState
Submodules *submodulePanelState
Suggestions *suggestionsPanelState
}
type searchingState struct {
@@ -297,6 +309,8 @@ type guiState struct {
Commits []*models.Commit
StashEntries []*models.StashEntry
CommitFiles []*models.CommitFile
// Suggestions will sometimes appear when typing into a prompt
Suggestions []*types.Suggestion
// FilteredReflogCommits are the ones that appear in the reflog panel.
// when in filtering mode we only include the ones that match the given path
FilteredReflogCommits []*models.Commit
@@ -383,6 +397,7 @@ func (gui *Gui) resetState() {
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}},
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
@@ -582,6 +597,7 @@ func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
go utils.Safe(func() {
for _, task := range tasks {
task := task
go utils.Safe(func() {
if err := task(done); err != nil {
_ = gui.surfaceError(err)
+21 -1
View File
@@ -359,6 +359,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.wrappedHandler(gui.handleCreateRecentReposMenu),
Description: gui.Tr.SwitchRepo,
},
{
ViewName: "status",
Key: gui.getKey(config.Status.AllBranchesLogGraph),
Handler: gui.wrappedHandler(gui.handleShowAllBranchLogs),
Description: gui.Tr.LcAllBranchesLogGraph,
},
{
ViewName: "files",
Contexts: []string{FILES_CONTEXT_KEY},
@@ -505,6 +511,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCreatePullRequestPress,
Description: gui.Tr.LcCreatePullRequest,
},
{
ViewName: "branches",
Contexts: []string{LOCAL_BRANCHES_CONTEXT_KEY},
Key: gui.getKey(config.Branches.CopyPullRequestURL),
Handler: gui.handleCopyPullRequestURLPress,
Description: gui.Tr.LcCopyPullRequestURL,
},
{
ViewName: "branches",
Contexts: []string{LOCAL_BRANCHES_CONTEXT_KEY},
@@ -821,6 +834,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.wrappedHandler(gui.exitCherryPickingMode),
Description: gui.Tr.LcResetCherryPick,
},
{
ViewName: "commits",
Contexts: []string{BRANCH_COMMITS_CONTEXT_KEY},
Key: gui.getKey(config.Commits.CopyCommitMessageToClipboard),
Handler: gui.wrappedHandler(gui.handleCopySelectedCommitMessageToClipboard),
Description: gui.Tr.LcCopyCommitMessageToClipboard,
},
{
ViewName: "commits",
Contexts: []string{REFLOG_COMMITS_CONTEXT_KEY},
@@ -964,7 +984,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
},
{
ViewName: "commitMessage",
Key: gui.getKey(config.Universal.Confirm),
Key: gui.getKey(config.Universal.SubmitEditorText),
Modifier: gocui.ModNone,
Handler: gui.handleCommitConfirm,
},
+18 -25
View File
@@ -34,7 +34,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.NotEnoughSpace
@@ -101,7 +101,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v, err := setViewFromDimensions("main", "main", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.DiffTitle
@@ -112,7 +112,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
secondaryView.Title = gui.Tr.DiffTitle
@@ -124,7 +124,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
hiddenViewOffset := 9999
if v, err := setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.StatusTitle
@@ -133,7 +133,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
filesView, err := setViewFromDimensions("files", "files", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
filesView.Highlight = true
@@ -144,7 +144,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView, err := setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
branchesView.Title = gui.Tr.BranchesTitle
@@ -154,7 +154,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitFilesView.Title = gui.Tr.CommitFiles
@@ -165,7 +165,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitsView, err := setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitsView.Title = gui.Tr.CommitsTitle
@@ -175,7 +175,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
stashView, err := setViewFromDimensions("stash", "stash", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
stashView.Title = gui.Tr.StashTitle
@@ -186,7 +186,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("commitMessage")
@@ -200,7 +200,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("credentials")
@@ -211,7 +211,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if v, err := setViewFromDimensions("options", "options", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Frame = false
@@ -225,7 +225,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// this view takes up one character. Its only purpose is to show the slash when searching
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
@@ -236,7 +236,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
@@ -247,7 +247,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
appStatusView.BgColor = gocui.ColorDefault
@@ -258,7 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
informationView, err := setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
informationView.BgColor = gocui.ColorDefault
@@ -277,7 +277,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
initialContext = gui.Contexts.BranchCommits.Context
}
if err := gui.switchContext(initialContext); err != nil {
if err := gui.pushContext(initialContext); err != nil {
return err
}
}
@@ -351,7 +351,7 @@ func (gui *Gui) onInitialViewsCreation() error {
}
gui.g.Mutexes.ViewsMutex.Unlock()
if err := gui.switchContext(gui.defaultSideContext()); err != nil {
if err := gui.pushContext(gui.defaultSideContext()); err != nil {
return err
}
@@ -368,10 +368,3 @@ func (gui *Gui) onInitialViewsCreation() error {
return gui.loadNewRepo()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
+20 -2
View File
@@ -219,7 +219,7 @@ func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
newSelectedLineIdx := v.SelectedLineIdx()
// we need to focus the view
if err := lc.Gui.switchContext(lc); err != nil {
if err := lc.Gui.pushContext(lc); err != nil {
return err
}
@@ -248,7 +248,7 @@ func (gui *Gui) menuListContext() *ListContext {
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: func() error { return gui.onMenuPress() },
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
@@ -483,6 +483,23 @@ func (gui *Gui) submodulesListContext() *ListContext {
}
}
func (gui *Gui) suggestionsListContext() *ListContext {
return &ListContext{
ViewName: "suggestions",
WindowName: "suggestions",
ContextKey: SUGGESTIONS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Suggestions) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
GetDisplayStrings: func() [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
gui.menuListContext(),
@@ -497,6 +514,7 @@ func (gui *Gui) getListContexts() []*ListContext {
gui.stashListContext(),
gui.commitFilesListContext(),
gui.submodulesListContext(),
gui.suggestionsListContext(),
}
}
+8 -11
View File
@@ -14,11 +14,6 @@ type viewUpdateOpts struct {
task updateTask
}
type coordinates struct {
x int
y int
}
type refreshMainOpts struct {
main *viewUpdateOpts
secondary *viewUpdateOpts
@@ -91,9 +86,10 @@ func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
return &runPtyTask{cmd: cmd}
}
func (gui *Gui) createRunPtyTaskWithPrefix(cmd *exec.Cmd, prefix string) *runPtyTask {
return &runPtyTask{cmd: cmd, prefix: prefix}
}
// currently unused
// func (gui *Gui) createRunPtyTaskWithPrefix(cmd *exec.Cmd, prefix string) *runPtyTask {
// return &runPtyTask{cmd: cmd, prefix: prefix}
// }
type runFunctionTask struct {
f func(chan struct{}) error
@@ -103,9 +99,10 @@ func (t *runFunctionTask) GetKind() int {
return RUN_FUNCTION
}
func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
return &runFunctionTask{f: f}
}
// currently unused
// func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
// return &runFunctionTask{f: f}
// }
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
switch task.GetKind() {
+1 -1
View File
@@ -88,7 +88,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchContext(gui.Contexts.Menu.Context)
return gui.pushContext(gui.Contexts.Menu.Context)
})
return nil
}
+16 -19
View File
@@ -50,9 +50,9 @@ func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top b
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
if len(conflicts) == 0 {
return content, nil
return content
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
@@ -71,7 +71,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
return outputBuffer.String()
}
func (gui *Gui) takeOverScrolling() {
@@ -142,7 +142,7 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
func (gui *Gui) pushFileSnapshot() error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
@@ -175,7 +175,7 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
if err := gui.pushFileSnapshot(g); err != nil {
if err := gui.pushFileSnapshot(); err != nil {
return err
}
@@ -201,7 +201,7 @@ func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
if err := gui.pushFileSnapshot(g); err != nil {
if err := gui.pushFileSnapshot(); err != nil {
return err
}
err := gui.resolveConflict(conflict, "both")
@@ -213,7 +213,7 @@ func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
cat, err := gui.catSelectedFile()
if err != nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -233,12 +233,9 @@ func (gui *Gui) refreshMergePanel() error {
}
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err != nil {
return err
}
content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err := gui.scrollToConflict(gui.g); err != nil {
if err := gui.scrollToConflict(); err != nil {
return err
}
@@ -251,7 +248,7 @@ func (gui *Gui) refreshMergePanel() error {
})
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
func (gui *Gui) catSelectedFile() (string, error) {
item := gui.getSelectedFile()
if item == nil {
return "", errors.New(gui.Tr.NoFilesDisplay)
@@ -269,7 +266,7 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
return cat, nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
func (gui *Gui) scrollToConflict() error {
if gui.State.Panels.Merging.UserScrolling {
return nil
}
@@ -312,13 +309,13 @@ func (gui *Gui) handleEscapeMerge() error {
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
}
return nil
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
if err := gui.stageSelectedFile(); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
@@ -326,7 +323,7 @@ func (gui *Gui) handleCompleteMerge() error {
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == "normal" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
@@ -345,14 +342,14 @@ func (gui *Gui) promptToContinue() error {
prompt: gui.Tr.ConflictsResolved,
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.switchContext(gui.Contexts.Files.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Files.Context); err != nil {
return err
}
return gui.genericMergeCommand("continue")
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
},
})
}
+1 -1
View File
@@ -110,7 +110,7 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
}
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
} else {
// need to re-focus in case the secondary view should now be hidden
return gui.currentContext().HandleFocus()
+4 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
@@ -26,7 +27,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
},
}
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == "normal" {
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == commands.REBASE_MODE_NORMAL {
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
@@ -74,7 +75,7 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != "normal" {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError)
}
return true, nil
@@ -179,7 +180,7 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.switchContext(gui.Contexts.CommitFiles.Context); err != nil {
if err := gui.pushContext(gui.Contexts.CommitFiles.Context); err != nil {
return err
}
}
+19
View File
@@ -0,0 +1,19 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func GetSuggestionListDisplayStrings(suggestions []*types.Suggestion) [][]string {
lines := make([][]string, len(suggestions))
for i := range suggestions {
lines[i] = getSuggestionDisplayStrings(suggestions[i])
}
return lines
}
func getSuggestionDisplayStrings(suggestion *types.Suggestion) []string {
return []string{suggestion.Label}
}
+1 -1
View File
@@ -40,7 +40,7 @@ func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack
return gui.switchContext(parentContext)
return gui.pushContext(parentContext)
}
for _, mode := range gui.modeStatuses() {
+7 -5
View File
@@ -3,12 +3,14 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{"continue", "abort"}
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
options = append(options, "skip")
}
@@ -25,7 +27,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
}
var title string
if gui.GitCommand.WorkingTreeState() == "merging" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle
} else {
title = gui.Tr.RebaseOptionsTitle
@@ -37,7 +39,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.GitCommand.WorkingTreeState()
if status != "merging" && status != "rebasing" {
if status != commands.REBASE_MODE_MERGING && status != commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing)
}
@@ -45,7 +47,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == "merging" && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
@@ -81,7 +83,7 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
prompt: gui.Tr.FoundConflicts,
handlersManageFocus: true,
handleConfirm: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
},
handleClose: func() error {
if err := gui.returnFromContext(); err != nil {
+14
View File
@@ -32,6 +32,20 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
return gui.createMenu(gui.Tr.RecentRepos, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleShowAllBranchLogs() error {
cmd := gui.OSCommand.ExecutableFromString(
gui.Config.GetUserConfig().Git.AllBranchesLogCmd,
)
task := gui.createRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Log",
task: task,
},
})
}
func (gui *Gui) dispatchSwitchToRepo(path string) error {
env.UnsetGitDirEnvs()
if err := os.Chdir(path); err != nil {
+1 -1
View File
@@ -21,7 +21,7 @@ func recordEventsTo() string {
}
func (gui *Gui) timeSinceStart() int64 {
return time.Since(gui.StartTime).Milliseconds()
return time.Since(gui.StartTime).Nanoseconds() / 1e6
}
func (gui *Gui) replayRecordedEvents() {
+3 -4
View File
@@ -40,7 +40,7 @@ func (gui *Gui) handleRemoteBranchSelect() error {
}
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
return gui.switchContext(gui.Contexts.Remotes.Context)
return gui.pushContext(gui.Contexts.Remotes.Context)
}
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
@@ -60,9 +60,8 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
prompt: message,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
if err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name); err != nil {
return err
}
err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
+46 -32
View File
@@ -81,18 +81,25 @@ func (gui *Gui) handleRemoteEnter() error {
}
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
return gui.switchContext(gui.Contexts.Remotes.Branches.Context)
return gui.pushContext(gui.Contexts.Remotes.Branches.Context)
}
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.LcNewRemoteName, "", func(remoteName string) error {
return gui.prompt(gui.Tr.LcNewRemoteUrl, "", func(remoteUrl string) error {
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
})
return gui.prompt(promptOpts{
title: gui.Tr.LcNewRemoteName,
handleConfirm: func(remoteName string) error {
return gui.prompt(promptOpts{
title: gui.Tr.LcNewRemoteUrl,
handleConfirm: func(remoteUrl string) error {
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
},
})
},
})
}
func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
@@ -106,7 +113,7 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
handleConfirm: func() error {
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
return err
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
@@ -127,32 +134,40 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.prompt(editNameMessage, remote.Name, func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name {
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.surfaceError(err)
return gui.prompt(promptOpts{
title: editNameMessage,
initialContent: remote.Name,
handleConfirm: func(updatedRemoteName string) error {
if updatedRemoteName != remote.Name {
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.surfaceError(err)
}
}
}
editUrlMessage := utils.ResolvePlaceholderString(
gui.Tr.LcEditRemoteUrl,
map[string]string{
"remoteName": updatedRemoteName,
},
)
editUrlMessage := utils.ResolvePlaceholderString(
gui.Tr.LcEditRemoteUrl,
map[string]string{
"remoteName": updatedRemoteName,
},
)
urls := remote.Urls
url := ""
if len(urls) > 0 {
url = urls[0]
}
return gui.prompt(editUrlMessage, url, func(updatedRemoteUrl string) error {
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.surfaceError(err)
urls := remote.Urls
url := ""
if len(urls) > 0 {
url = urls[0]
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
return gui.prompt(promptOpts{
title: editUrlMessage,
initialContent: url,
handleConfirm: func(updatedRemoteUrl string) error {
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
},
})
},
})
}
@@ -166,7 +181,6 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
gui.Mutexes.FetchMutex.Lock()
defer gui.Mutexes.FetchMutex.Unlock()
// TODO: test this
err := gui.GitCommand.FetchRemote(remote.Name, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
+1 -1
View File
@@ -17,7 +17,7 @@ func (gui *Gui) resetToRef(ref string, strength string, options oscommands.RunCo
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.switchContext(gui.Contexts.BranchCommits.Context); err != nil {
if err := gui.pushContext(gui.Contexts.BranchCommits.Context); err != nil {
return err
}
+1 -1
View File
@@ -14,7 +14,7 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.renderString("search", "")
if err := gui.switchContext(gui.Contexts.Search.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Search.Context); err != nil {
return err
}
+3 -3
View File
@@ -25,7 +25,7 @@ func (gui *Gui) nextSideWindow() error {
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
return gui.pushContextWithView(viewName)
}
func (gui *Gui) previousSideWindow() error {
@@ -51,11 +51,11 @@ func (gui *Gui) previousSideWindow() error {
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
return gui.pushContextWithView(viewName)
}
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return gui.switchContextToView(sideViewName)
return gui.pushContextWithView(sideViewName)
}
}
+5 -7
View File
@@ -18,10 +18,8 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
secondaryFocused := false
if forceSecondaryFocused {
secondaryFocused = true
} else {
if state != nil {
secondaryFocused = state.SecondaryFocused
}
} else if state != nil {
secondaryFocused = state.SecondaryFocused
}
if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) {
@@ -87,7 +85,7 @@ func (gui *Gui) handleTogglePanel() error {
func (gui *Gui) handleStagingEscape() error {
gui.escapeLineByLinePanel()
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
}
func (gui *Gui) handleToggleStagedSelection() error {
@@ -110,7 +108,7 @@ func (gui *Gui) handleResetSelection() error {
handlersManageFocus: true,
handleConfirm: func() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if err := gui.switchContext(gui.Contexts.Staging.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Staging.Context); err != nil {
return err
}
@@ -118,7 +116,7 @@ func (gui *Gui) handleResetSelection() error {
})
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Staging.Context)
return gui.pushContext(gui.Contexts.Staging.Context)
},
})
} else {
+9 -5
View File
@@ -117,11 +117,15 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.Tr.NoTrackedStagedFilesStash)
}
return gui.prompt(gui.Tr.StashChanges, "", func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
return gui.prompt(promptOpts{
title: gui.Tr.StashChanges,
handleConfirm: func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
},
})
}
+8 -7
View File
@@ -6,6 +6,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -33,7 +34,7 @@ func (gui *Gui) refreshStatus() {
status = utils.ColoredString(fmt.Sprintf("↑%s↓%s ", currentBranch.Pushables, currentBranch.Pullables), trackColor)
}
if gui.GitCommand.WorkingTreeState() != "normal" {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
status += utils.ColoredString(fmt.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()), color.FgYellow)
}
@@ -57,7 +58,7 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(v, gui.Tr.CheckingForUpdates)
return gui.createLoaderPanel(gui.Tr.CheckingForUpdates)
}
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
@@ -72,7 +73,7 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
return nil
}
if err := gui.switchContext(gui.Contexts.Status.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Status.Context); err != nil {
return err
}
@@ -80,7 +81,7 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
repoName := utils.GetCurrentRepoName()
switch gui.GitCommand.WorkingTreeState() {
case "rebasing", "merging":
case commands.REBASE_MODE_REBASING, commands.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return gui.handleCreateRebaseOptionsMenu()
@@ -149,11 +150,11 @@ func lazygitTitle() string {
func (gui *Gui) workingTreeState() string {
rebaseMode, _ := gui.GitCommand.RebaseMode()
if rebaseMode != "" {
return "rebasing"
return commands.REBASE_MODE_REBASING
}
merging, _ := gui.GitCommand.IsInMergeState()
if merging {
return "merging"
return commands.REBASE_MODE_MERGING
}
return "normal"
return commands.REBASE_MODE_NORMAL
}
+1 -1
View File
@@ -97,7 +97,7 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
return gui.switchContext(gui.Contexts.SubCommits.Context)
return gui.pushContext(gui.Contexts.SubCommits.Context)
}
func (gui *Gui) handleSwitchToSubCommits() error {
+33 -16
View File
@@ -126,30 +126,47 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
}
func (gui *Gui) handleAddSubmodule() error {
return gui.prompt(gui.Tr.LcNewSubmoduleUrl, "", func(submoduleUrl string) error {
nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl)))
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmoduleUrl,
handleConfirm: func(submoduleUrl string) error {
nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl)))
return gui.prompt(gui.Tr.LcNewSubmoduleName, nameSuggestion, func(submoduleName string) error {
return gui.prompt(gui.Tr.LcNewSubmodulePath, submoduleName, func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmoduleName,
initialContent: nameSuggestion,
handleConfirm: func(submoduleName string) error {
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmodulePath,
initialContent: submoduleName,
handleConfirm: func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
},
})
},
})
})
},
})
}
func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error {
return gui.prompt(fmt.Sprintf(gui.Tr.LcUpdateSubmoduleUrl, submodule.Name), submodule.Url, func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.prompt(promptOpts{
title: fmt.Sprintf(gui.Tr.LcUpdateSubmoduleUrl, submodule.Name),
initialContent: submodule.Url,
handleConfirm: func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
},
})
}
+36
View File
@@ -0,0 +1,36 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) getSelectedSuggestionValue() string {
selectedSuggestion := gui.getSelectedSuggestion()
if selectedSuggestion != nil {
return selectedSuggestion.Value
}
return ""
}
func (gui *Gui) getSelectedSuggestion() *types.Suggestion {
selectedLine := gui.State.Panels.Suggestions.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.Suggestions[selectedLine]
}
func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) {
view := gui.getSuggestionsView()
if view == nil {
return
}
gui.State.Suggestions = suggestions
gui.State.Panels.Suggestions.SelectedLineIdx = 0
_ = gui.resetOrigin(view)
_ = gui.Contexts.Suggestions.Context.HandleRender()
}
+32 -23
View File
@@ -56,7 +56,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.switchContext(gui.Contexts.Branches.Context)
return gui.pushContext(gui.Contexts.Branches.Context)
}
func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
@@ -97,34 +97,43 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.prompt(title, "origin", func(response string) error {
if err := gui.GitCommand.PushTag(response, tag.Name); err != nil {
return gui.surfaceError(err)
}
return nil
return gui.prompt(promptOpts{
title: title,
initialContent: "origin",
handleConfirm: func(response string) error {
return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error {
err := gui.GitCommand.PushTag(response, tag.Name, gui.promptUserForCredential)
gui.handleCredentialsPopup(err)
return nil
})
},
})
}
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.CreateTagTitle, "", func(tagName string) error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS, TAGS}, then: func() {
// find the index of the tag and set that as the currently selected line
for i, tag := range gui.State.Tags {
if tag.Name == tagName {
gui.State.Panels.Tags.SelectedLineIdx = i
if err := gui.Contexts.Tags.Context.HandleRender(); err != nil {
gui.Log.Error(err)
}
return
}
return gui.prompt(promptOpts{
title: gui.Tr.CreateTagTitle,
handleConfirm: func(tagName string) error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS, TAGS}, then: func() {
// find the index of the tag and set that as the currently selected line
for i, tag := range gui.State.Tags {
if tag.Name == tagName {
gui.State.Panels.Tags.SelectedLineIdx = i
if err := gui.Contexts.Tags.Context.HandleRender(); err != nil {
gui.Log.Error(err)
}
return
}
}
},
})
},
})
})
}
+8
View File
@@ -0,0 +1,8 @@
package types
type Suggestion struct {
// value is the thing that we're matching on and the thing that will be submitted if you select the suggestion
Value string
// label is what is actually displayed so it can e.g. contain color
Label string
}
+3 -2
View File
@@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -87,7 +88,7 @@ func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.UndoingStatus
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing)
}
@@ -118,7 +119,7 @@ func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.RedoingStatus
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing)
}
+10 -6
View File
@@ -1,11 +1,15 @@
package gui
import "github.com/jesseduffield/gocui"
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) showUpdatePrompt(newVersion string) error {
return gui.ask(askOpts{
title: "New version available!",
prompt: "Download latest version? (enter/esc)",
prompt: fmt.Sprintf("Download version %s? (enter/esc)", newVersion),
handleConfirm: func() error {
gui.startUpdating(newVersion)
return nil
@@ -41,13 +45,13 @@ func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) erro
func (gui *Gui) startUpdating(newVersion string) {
gui.State.Updating = true
gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, gui.onUpdateFinish)
statusId := gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, func(err error) error { return gui.onUpdateFinish(statusId, err) })
}
func (gui *Gui) onUpdateFinish(err error) error {
func (gui *Gui) onUpdateFinish(statusId int, err error) error {
gui.State.Updating = false
gui.statusManager.removeStatus("updating")
gui.statusManager.removeStatus(statusId)
gui.renderString("appStatus", "")
if err != nil {
return gui.createErrorPanel("Update failed: " + err.Error())
+26 -24
View File
@@ -110,9 +110,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshCommits() })
go utils.Safe(func() { _ = gui.refreshCommits() })
} else {
gui.refreshCommits()
_ = gui.refreshCommits()
}
wg.Done()
}()
@@ -122,9 +122,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshFilesAndSubmodules() })
go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() })
} else {
gui.refreshFilesAndSubmodules()
_ = gui.refreshFilesAndSubmodules()
}
wg.Done()
}()
@@ -134,9 +134,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshStashEntries() })
go utils.Safe(func() { _ = gui.refreshStashEntries() })
} else {
gui.refreshStashEntries()
_ = gui.refreshStashEntries()
}
wg.Done()
}()
@@ -146,9 +146,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshTags() })
go utils.Safe(func() { _ = gui.refreshTags() })
} else {
gui.refreshTags()
_ = gui.refreshTags()
}
wg.Done()
}()
@@ -158,9 +158,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshRemotes() })
go utils.Safe(func() { _ = gui.refreshRemotes() })
} else {
gui.refreshRemotes()
_ = gui.refreshRemotes()
}
wg.Done()
}()
@@ -244,11 +244,6 @@ func (gui *Gui) getFilesView() *gocui.View {
return v
}
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
@@ -264,20 +259,27 @@ func (gui *Gui) getMainView() *gocui.View {
return v
}
func (gui *Gui) getSuggestionsView() *gocui.View {
v, _ := gui.g.View("suggestions")
return v
}
func (gui *Gui) getSecondaryView() *gocui.View {
v, _ := gui.g.View("secondary")
return v
}
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
// currently unused
// func (gui *Gui) getStashView() *gocui.View {
// v, _ := gui.g.View("stash")
// return v
// }
func (gui *Gui) getCommitFilesView() *gocui.View {
v, _ := gui.g.View("commitFiles")
return v
}
// currently unused
// func (gui *Gui) getCommitFilesView() *gocui.View {
// v, _ := gui.g.View("commitFiles")
// return v
// }
func (gui *Gui) getMenuView() *gocui.View {
v, _ := gui.g.View("menu")
@@ -418,7 +420,7 @@ func (gui *Gui) clearEditorView(v *gocui.View) {
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
context := gui.ViewTabContextMap[viewName][tabIndex].contexts[0]
return gui.switchContext(context)
return gui.pushContext(context)
}
func (gui *Gui) handleNextTab(g *gocui.Gui, v *gocui.View) error {
+6
View File
@@ -152,8 +152,10 @@ func dutchTranslationSet() TranslationSet {
LcMergeIntoCurrentBranch: `merge in met huidige checked out branch`,
ConfirmQuit: `Weet je zeker dat je dit programma wil sluiten?`,
SwitchRepo: "wissel naar een recente repo",
LcAllBranchesLogGraph: `alle takken van het houtblok laten zien`,
UnsupportedGitService: `Niet-ondersteunde git-service`,
LcCreatePullRequest: `maak een pull-aanvraag`,
LcCopyPullRequestURL: `kopieer de URL van het pull-verzoek naar het klembord`,
NoBranchOnRemote: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
LcFetch: `fetch`,
NoAutomaticGitFetchTitle: `Geen automatische git fetch`,
@@ -371,6 +373,7 @@ func dutchTranslationSet() TranslationSet {
LcOpenDiffingMenu: "open diff menu",
LcShowingGitDiff: "laat output zien voor:",
LcCopyCommitShaToClipboard: "copieer commit SHA naar clipboard",
LcCopyCommitMessageToClipboard: "copieer commit bericht naar clipboard",
LcCopyBranchNameToClipboard: "copieer branch name naar clipboard",
LcCopyFileNameToClipboard: "kopieer de bestandsnaam naar het klembord",
LcCopyCommitFileNameToClipboard: "kopieer de vastgelegde bestandsnaam naar het klembord",
@@ -379,5 +382,8 @@ func dutchTranslationSet() TranslationSet {
NoFilesStagedPrompt: "Je hebt geen bestanden gestaged. Commit alle bestanden?",
BranchNotFoundTitle: "Branch niet gevonden",
BranchNotFoundPrompt: "Branch niet gevonden. Creëer een nieuwe branch genaamd",
PullRequestURLCopiedToClipboard: "Pull-aanvraag-URL gekopieerd naar klembord",
CommitMessageCopiedToClipboard: "Commit message gekopieerd naar klembord",
LcCopiedToClipboard: "gekopieerd naar klembord",
}
}
+41 -1
View File
@@ -164,8 +164,10 @@ type TranslationSet struct {
LcMergeIntoCurrentBranch string
ConfirmQuit string
SwitchRepo string
LcAllBranchesLogGraph string
UnsupportedGitService string
LcCreatePullRequest string
LcCopyPullRequestURL string
NoBranchOnRemote string
LcFetch string
NoAutomaticGitFetchTitle string
@@ -381,6 +383,7 @@ type TranslationSet struct {
LcOpenDiffingMenu string
LcShowingGitDiff string
LcCopyCommitShaToClipboard string
LcCopyCommitMessageToClipboard string
LcCopyBranchNameToClipboard string
LcCopyFileNameToClipboard string
LcCopyCommitFileNameToClipboard string
@@ -427,9 +430,39 @@ type TranslationSet struct {
SubCommitsTitle string
SubmodulesTitle string
NavigationTitle string
PushingTagStatus string
PullRequestURLCopiedToClipboard string
CommitMessageCopiedToClipboard string
LcCopiedToClipboard string
}
const englishReleaseNotes = `## lazygit 0.23.2 Release Notes
const englishReleaseNotes = `## lazygit 0.24 Release Notes
- Suggestions now shown when checking out branch by name
- Minimum OSX version is now officially 10.10
- Pull requests URLs can be copied from the keyboard, thanks @farzadmf!
- Allow --follow-tags flag for git push to be disabled in config,
thanks @fishybell!
- Allow quick commit when no files are staged and the user presses 'c',
thanks @fluffynuts!
- Lazygit config is now by default created with 'jesseduffield' as the parent
folder, thanks @Liberatys!
- You can now configure how lazygit behaves when you open it outside a repo
(e.g. skip the prompt and open the most recent repo), thanks @kalvinpearce!
- You can now visualise the commit graph for all branches by pressing 'a' in
the status panel - thanks @Yuuki77!
- And thanks to @dawidd6, @sstiglitz, @fargozhu and @nils-a for helping out with
CI and documentation!
## lazygit 0.23.2 Release Notes
- Fixed bug where editing a file with spaces did not work
- Fixed formatting issue with delta that rendered '[0;K' to the screen
@@ -660,8 +693,10 @@ func englishTranslationSet() TranslationSet {
LcMergeIntoCurrentBranch: `merge into currently checked out branch`,
ConfirmQuit: `Are you sure you want to quit?`,
SwitchRepo: `switch to a recent repo`,
LcAllBranchesLogGraph: `show all branch logs`,
UnsupportedGitService: `Unsupported git service`,
LcCreatePullRequest: `create pull request`,
LcCopyPullRequestURL: `copy pull request URL to clipboard`,
NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`,
LcFetch: `fetch`,
NoAutomaticGitFetchTitle: `No automatic git fetch`,
@@ -878,6 +913,7 @@ func englishTranslationSet() TranslationSet {
LcOpenDiffingMenu: "open diff menu",
LcShowingGitDiff: "showing output for:",
LcCopyCommitShaToClipboard: "copy commit SHA to clipboard",
LcCopyCommitMessageToClipboard: "copy commit message to clipboard",
LcCopyBranchNameToClipboard: "copy branch name to clipboard",
LcCopyFileNameToClipboard: "copy the file name to the clipboard",
LcCopyCommitFileNameToClipboard: "copy the committed file name to the clipboard",
@@ -924,5 +960,9 @@ func englishTranslationSet() TranslationSet {
SubCommitsTitle: "Sub-commits",
SubmodulesTitle: "Submodules",
NavigationTitle: "List Panel Navigation",
PushingTagStatus: "pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",
CommitMessageCopiedToClipboard: "Commit message copied to clipboard",
LcCopiedToClipboard: "copied to clipboard",
}
}
-8
View File
@@ -2,19 +2,11 @@ package i18n
import (
"fmt"
"io/ioutil"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func getDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// TestDetectLanguage is a function.
func TestDetectLanguage(t *testing.T) {
type scenario struct {
+5
View File
@@ -125,8 +125,10 @@ func polishTranslationSet() TranslationSet {
LcRefreshFiles: `odśwież pliki`,
LcMergeIntoCurrentBranch: `scal do obecnej gałęzi`,
ConfirmQuit: `Na pewno chcesz wyjść z programu?`,
LcAllBranchesLogGraph: `pokazywać wszystkie logi branżowe`,
UnsupportedGitService: `Nieobsługiwana usługa git`,
LcCreatePullRequest: `utwórz żądanie wyciągnięcia`,
LcCopyPullRequestURL: `skopiuj adres URL żądania ściągnięcia do schowka`,
NoBranchOnRemote: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
LcFetch: `fetch`,
NoAutomaticGitFetchTitle: `No automatic git fetch`,
@@ -250,5 +252,8 @@ func polishTranslationSet() TranslationSet {
NoFilesStagedPrompt: "You have not staged any files. Commit all files?",
BranchNotFoundTitle: "Branch not found",
BranchNotFoundPrompt: "Branch not found. Create a new branch named",
PullRequestURLCopiedToClipboard: "URL żądania ściągnięcia skopiowany do schowka",
CommitMessageCopiedToClipboard: "Commit message skopiowany do schowka",
LcCopiedToClipboard: "skopiowany do schowka",
}
}
+42 -12
View File
@@ -92,9 +92,18 @@ func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
}
func (u *Updater) currentVersion() string {
versionNumber := u.Config.GetVersion()
if versionNumber == "unversioned" {
return versionNumber
}
return fmt.Sprintf("v%s", u.Config.GetVersion())
}
func (u *Updater) checkForNewUpdate() (string, error) {
u.Log.Info("Checking for an updated version")
currentVersion := u.Config.GetVersion()
currentVersion := u.currentVersion()
if err := u.RecordLastUpdateCheck(); err != nil {
return "", err
}
@@ -214,12 +223,16 @@ func (u *Updater) mappedArch(arch string) string {
return arch
}
func (u *Updater) zipExtension() string {
if runtime.GOOS == "windows" {
return "zip"
}
return "tar.gz"
}
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
extension := "tar.gz"
if runtime.GOOS == "windows" {
extension = "zip"
}
url := fmt.Sprintf(
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
PROJECT_URL,
@@ -227,7 +240,7 @@ func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
newVersion[1:],
u.mappedOs(runtime.GOOS),
u.mappedArch(runtime.GOARCH),
extension,
u.zipExtension(),
)
u.Log.Info("Url for latest release is " + url)
return url, nil
@@ -256,11 +269,16 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
configDir := u.Config.GetUserConfigDir()
u.Log.Info("Download directory is " + configDir)
tempPath := filepath.Join(configDir, "temp_lazygit")
u.Log.Info("Temp path to binary is " + tempPath)
zipPath := filepath.Join(configDir, "temp_lazygit."+u.zipExtension())
u.Log.Info("Temp path to tarball/zip file is " + zipPath)
// Create the file
out, err := os.Create(tempPath)
// remove existing zip file
if err := os.RemoveAll(zipPath); err != nil && !os.IsNotExist(err) {
return err
}
// Create the zip file
out, err := os.Create(zipPath)
if err != nil {
return err
}
@@ -284,6 +302,18 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
return err
}
u.Log.Info("untarring tarball/unzipping zip file")
if err := u.OSCommand.RunCommand("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit"); err != nil {
return err
}
// the `tar` terminal cannot store things in a new location without permission
// so it creates it in the current directory. As such our path is fairly simple.
// You won't see it because it's gitignored.
tempLazygitFilePath := "lazygit"
u.Log.Infof("Path to temp binary is %s", tempLazygitFilePath)
// get the path of the current binary
binaryPath, err := osext.Executable()
if err != nil {
@@ -292,12 +322,12 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
u.Log.Info("Binary path is " + binaryPath)
// Verify the main file exists
if _, err := os.Stat(tempPath); err != nil {
if _, err := os.Stat(zipPath); err != nil {
return err
}
// swap out the old binary for the new one
err = os.Rename(tempPath, binaryPath)
err = os.Rename(tempLazygitFilePath, binaryPath)
if err != nil {
return err
}
+23
View File
@@ -0,0 +1,23 @@
package utils
import (
"sort"
"github.com/sahilm/fuzzy"
)
func FuzzySearch(needle string, haystack []string) []string {
if needle == "" {
return []string{}
}
matches := fuzzy.Find(needle, haystack)
sort.Sort(matches)
result := make([]string, len(matches))
for i, match := range matches {
result[i] = match.Str
}
return result
}
+53
View File
@@ -0,0 +1,53 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestFuzzySearch is a function.
func TestFuzzySearch(t *testing.T) {
type scenario struct {
needle string
haystack []string
expected []string
}
scenarios := []scenario{
{
needle: "",
haystack: []string{"test"},
expected: []string{},
},
{
needle: "test",
haystack: []string{"test"},
expected: []string{"test"},
},
{
needle: "o",
haystack: []string{"a", "o", "e"},
expected: []string{"o"},
},
{
needle: "mybranch",
haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
expected: []string{"mybranch", "my_branch", "this is my branch"},
},
{
needle: "test",
haystack: []string{"not a good match", "this 'test' is a good match", "test"},
expected: []string{"test", "this 'test' is a good match"},
},
{
needle: "test",
haystack: []string{"Test"},
expected: []string{"Test"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
}
}
+2 -2
View File
@@ -102,8 +102,8 @@ func Loader() string {
// ResolvePlaceholderString populates a template with values
func ResolvePlaceholderString(str string, arguments map[string]string) string {
for key, value := range arguments {
str = strings.ReplaceAll(str, "{{"+key+"}}", value)
str = strings.ReplaceAll(str, "{{."+key+"}}", value)
str = strings.Replace(str, "{{"+key+"}}", value, -1)
str = strings.Replace(str, "{{."+key+"}}", value, -1)
}
return str
}
@@ -0,0 +1 @@
initial commit
@@ -0,0 +1 @@
ref: refs/heads/three
@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
@@ -0,0 +1,7 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.DS_Store
@@ -0,0 +1,6 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 commit (initial): initial commit
337bfd3b397e5d29e526f25ed4fb6094f857eada 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 checkout: moving from master to one
337bfd3b397e5d29e526f25ed4fb6094f857eada 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 checkout: moving from one to two
337bfd3b397e5d29e526f25ed4fb6094f857eada 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 checkout: moving from two to three
337bfd3b397e5d29e526f25ed4fb6094f857eada 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 checkout: moving from three to four
337bfd3b397e5d29e526f25ed4fb6094f857eada 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556588 +1100 checkout: moving from four to three
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 commit (initial): initial commit
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD
@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD

Some files were not shown because too many files have changed in this diff Show More