Compare commits

..

48 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
115 changed files with 2141 additions and 783 deletions

View File

@@ -6,7 +6,7 @@ on:
- 'v*'
jobs:
cd:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
@@ -21,11 +21,17 @@ 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:
@@ -35,11 +41,12 @@ jobs:
- 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 -S -m "d/changelog: dch $version"
git commit -m "d/changelog: dch $version"
gbp tag
git push --tags origin master

14
.github/workflows/lint.yml vendored Normal file
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
.gitignore vendored
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

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)

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'
@@ -339,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'
```

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
docs/README.md Normal file
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
go.mod
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
go.sum
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=

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 {

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
}

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
}

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

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

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

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
}

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

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
}

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 {

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

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

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

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

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"`
@@ -149,8 +152,9 @@ type KeybindingUniversalConfig struct {
}
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 {
@@ -169,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"`
@@ -279,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{
@@ -296,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),
},
@@ -366,8 +372,9 @@ func GetDefaultConfig() *UserConfig {
AppendNewline: "<tab>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
RecentRepos: "<enter>",
CheckForUpdate: "u",
RecentRepos: "<enter>",
AllBranchesLogGraph: "a",
},
Files: KeybindingFilesConfig{
CommitChanges: "c",
@@ -383,6 +390,7 @@ func GetDefaultConfig() *UserConfig {
Fetch: "f",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
CreatePullRequest: "o",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
@@ -438,5 +446,6 @@ func GetDefaultConfig() *UserConfig {
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
NotARepository: "prompt",
}
}

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 {

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

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
}

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

View File

@@ -79,43 +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) {
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()
}

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}})
},
})
}
@@ -602,5 +596,12 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
if err != nil {
return gui.surfaceError(err)
}
return gui.OSCommand.CopyToClipboard(message)
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
return nil
}

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 {

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

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

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 {

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

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
pkg/gui/editors.go Normal file
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)
}
}

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

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

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

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

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

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
}

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)

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

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
}

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

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

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
}

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

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

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

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

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

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 {

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 {

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

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 {

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

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
}

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
}

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

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 {

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

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
}

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 {

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

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

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,36 +97,43 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
},
)
return gui.prompt(title, "origin", 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 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
})
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
}
}
},
})
},
})
})
}

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
}

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

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

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 {

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`,
@@ -380,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",
}
}

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
@@ -429,9 +431,38 @@ type TranslationSet struct {
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
@@ -662,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`,
@@ -928,5 +961,8 @@ func englishTranslationSet() TranslationSet {
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",
}
}

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 {

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

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
pkg/utils/fuzzy_search.go Normal file
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
}

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

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
}

View File

@@ -0,0 +1 @@
initial commit

View File

@@ -0,0 +1 @@
ref: refs/heads/three

View File

@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI

View File

@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

View File

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

View File

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

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 commit (initial): initial commit

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD

View File

@@ -0,0 +1 @@
0000000000000000000000000000000000000000 337bfd3b397e5d29e526f25ed4fb6094f857eada CI <CI@example.com> 1606556586 +1100 branch: Created from HEAD

View File

@@ -0,0 +1 @@
337bfd3b397e5d29e526f25ed4fb6094f857eada

View File

@@ -0,0 +1 @@
337bfd3b397e5d29e526f25ed4fb6094f857eada

View File

@@ -0,0 +1 @@
337bfd3b397e5d29e526f25ed4fb6094f857eada

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