Compare commits

...
55 Commits
Author SHA1 Message Date
Daniel Bast b726dcc770 Switch to Go 1.16 to support macOS arm64 2021-02-23 03:04:49 -08:00
Matthias Küch 9df133ed8c Fix pattern in commitPrefix example 2021-02-16 13:57:28 -08:00
1jz 50dd7b00c3 add colors to differentiate action and menu commands 2021-02-16 13:52:04 -08:00
Rui Pires ccbd2c924b Fixed whitespace format issue 2021-02-09 14:45:33 -08:00
Rui Pires 52d5c3beeb Added initialContent on branch rename 2021-02-09 14:45:33 -08:00
Jesse Duffield c43416891e update cheatsheet 2021-02-09 20:23:20 +11:00
Jesse Duffield 56a573de86 support wide characters in the editor 2021-02-08 22:55:11 +00:00
Jesse Duffield e7fff2529c fix lint error 2021-02-08 14:40:30 -08:00
Jesse Duffield 78867647d1 remove go-gitconfig package 2021-02-08 14:40:30 -08:00
Jesse Duffield 09f32d4f84 add secureexec file for getting around windows checking for a binary first in the current dir 2021-02-08 14:40:30 -08:00
Nick Flueckiger 6f0f70bd92 Adding setup and config 2021-02-08 14:25:24 -08:00
caquillo07 6df15ddf6e added support for using spaces on branch names when creating new ones. 2021-02-08 14:23:54 -08:00
unknown 922c0887f1 fix type: executable not found error when there is a merge conflict on windows 2021-01-01 13:17:29 -08:00
Dawid Dziurla d7c9243880 workflows: setup git before PPA repo updating 2020-12-24 10:32:50 +01:00
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
151 changed files with 4700 additions and 1322 deletions
+13 -4
View File
@@ -6,8 +6,8 @@ on:
- 'v*'
jobs:
cd:
runs-on: ubuntu-20.04
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -16,25 +16,34 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.16.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
homebrew:
runs-on: macos-latest
steps:
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
ppa:
runs-on: ubuntu-20.04
steps:
- name: Checkout PPA repo
uses: actions/checkout@v2
with:
repository: dawidd6/lazygit-debian
token: ${{secrets.GITHUB_API_TOKEN}}
fetch-depth: 0
- name: Setup git
uses: dawidd6/action-git-user-config@v1
- 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"
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
go-version: 1.16.x
- name: Cache build
uses: actions/cache@v1
with:
+14
View File
@@ -0,0 +1,14 @@
name: Lint
on: pull_request
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Lint
uses: golangci/golangci-lint-action@v2
with:
version: latest
+1
View File
@@ -31,3 +31,4 @@ test/integration/*/used_config/
# these sample hooks waste too space space
test/integration/*/expected/.git_keep/hooks/
!.git_keep/
lazygit.exe
+10 -1
View File
@@ -30,6 +30,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
- [Chocolatey (Windows)](#chocolatey-windows)
- [Manual](#manual)
- [Usage](#usage)
- [Keybindings](#keybindings)
@@ -52,7 +53,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
### Homebrew
@@ -166,6 +167,14 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
### Chocolatey (Windows)
You can install `lazygit` using [Chocolatey](https://chocolatey.org/):
```sh
choco install lazygit
```
### Manual
You'll need to [install Go](https://golang.org/doc/install)
+27 -1
View File
@@ -48,8 +48,12 @@ 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
refresher:
refreshInterval: 10 # file/submodule refresh interval in seconds
fetchInterval: 60 # re-fetch interval in seconds
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
@@ -58,6 +62,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'
@@ -323,7 +328,7 @@ Example:
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+)"
pattern: "^\\w+\\/(\\w+-\\w+).*"
replace: "[$1] "
```
@@ -339,3 +344,24 @@ git:
Result:
![](https://i.imgur.com/Nibq35B.png)
## Launching not in a repository behaviour
By default, when launching lazygit from a directory that is not a repository,
you will be prompted to choose if you would like to initialize a repo. You can
override this behaviour in the config with one of the following:
```yaml
# for default prompting behaviour
notARepository: 'prompt'
```
```yaml
# to skip and initialize a new repo
notARepository: 'create'
```
```yaml
# to skip without creating a new repo
notARepository: 'skip'
```
+3 -3
View File
@@ -2,7 +2,7 @@
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
```
```yml
customCommands:
- key: '<c-r>'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
@@ -95,7 +95,7 @@ The permitted option fields are:
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```
```yml
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
@@ -123,7 +123,7 @@ SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions
+7
View File
@@ -0,0 +1,7 @@
# Documentation Overview
* [Configuration](./Config.md).
* [Custom Commands](./Custom_Command_Keybindings.md)
* [Custom Pagers](./Custom_Pagers.md)
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)
+8 -5
View File
@@ -24,13 +24,13 @@
## List Panel Navigation
<pre>
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Branches Tab)
@@ -38,6 +38,7 @@
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
@@ -137,6 +138,7 @@
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
</pre>
## Commits Panel (Reflog Tab)
@@ -267,4 +269,5 @@
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: show all branch logs
</pre>
+8 -5
View File
@@ -24,13 +24,13 @@
## List Panel Navigation
<pre>
<kbd>.</kbd>: volgende pagina
<kbd>,</kbd>: vorige pagina
<kbd><</kbd>: scroll naar boven
<kbd>></kbd>: scroll naar beneden
<kbd>/</kbd>: start met zoekken
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
</pre>
## Branches Paneel (Branches Tab)
@@ -38,6 +38,7 @@
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
@@ -137,6 +138,7 @@
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
</pre>
## Commits Paneel (Reflog Tab)
@@ -267,4 +269,5 @@
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>a</kbd>: alle takken van het houtblok laten zien
</pre>
+8 -5
View File
@@ -24,13 +24,13 @@
## List Panel Navigation
<pre>
<kbd>.</kbd>: next page
<kbd>,</kbd>: previous page
<kbd><</kbd>: scroll to top
<kbd>></kbd>: scroll to bottom
<kbd>/</kbd>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Branches Tab)
@@ -38,6 +38,7 @@
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
@@ -137,6 +138,7 @@
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+y</kbd>: copy commit message to clipboard
</pre>
## Commity Panel (Reflog Tab)
@@ -267,4 +269,5 @@
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
<kbd>a</kbd>: pokazywać wszystkie logi branżowe
</pre>
+7 -3
View File
@@ -6,6 +6,7 @@ require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.2
github.com/aybabtme/humanlog v0.4.1
github.com/cli/safeexec v1.0.0
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
@@ -18,20 +19,23 @@ require (
github.com/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
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/mattn/go-runewidth v0.0.10
github.com/mgutz/str v1.2.0
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 // indirect
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/rivo/uniseg v0.2.0 // 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
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
+18 -2
View File
@@ -11,6 +11,8 @@ github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -63,8 +65,12 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
@@ -86,6 +92,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=
@@ -99,12 +107,16 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 h1:3vzIuru1svOK2sXlg4XcrO3KkGRneIejmfQfR+ptSW8=
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -114,6 +126,12 @@ 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/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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=
@@ -126,8 +144,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+16 -6
View File
@@ -8,7 +8,6 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@@ -21,6 +20,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
)
@@ -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 {
@@ -314,7 +324,7 @@ func TailLogs() {
log.Fatal(err)
}
cmd := exec.Command("tail", "-f", logFilePath)
cmd := secureexec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
+2 -6
View File
@@ -43,10 +43,6 @@ 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 ""
}
return strings.TrimSpace(output)
output, _ := c.getGitConfigValue(key)
return output
}
+6 -7
View File
@@ -15,12 +15,11 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
return &GitCommand{
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Config: config.NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
Log: utils.NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Config: config.NewDummyAppConfig(),
getGitConfigValue: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}
+27
View File
@@ -2,6 +2,7 @@ package commands
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
)
// CatFile obtains the content of a file
@@ -262,3 +264,28 @@ func (c *GitCommand) ResetAndClean() error {
return c.RemoveUntrackedFiles()
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
editor := c.GetConfigValue("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
}
+10 -13
View File
@@ -16,7 +16,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// this takes something like:
@@ -32,8 +31,7 @@ type GitCommand struct {
Repo *gogit.Repository
Tr *i18n.TranslationSet
Config config.AppConfigurer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
getGitConfigValue func(string) (string, error)
removeFile func(string) error
DotGitDir string
onSuccessfulContinue func() error
@@ -74,16 +72,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
}
gitCommand := &GitCommand{
Log: log,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
removeFile: os.RemoveAll,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
Log: log,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
getGitConfigValue: getGitConfigValue,
removeFile: os.RemoveAll,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
+271 -111
View File
@@ -16,6 +16,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
@@ -269,7 +270,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
{
"No stash entries found",
func(string, ...string) *exec.Cmd {
return exec.Command("echo")
return secureexec.Command("echo")
},
func(entries []*models.StashEntry) {
assert.Len(t, entries, 0)
@@ -278,7 +279,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
{
"Several stash entries found",
func(string, ...string) *exec.Cmd {
return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []*models.StashEntry) {
expected := []*models.StashEntry{
@@ -320,7 +321,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
{
"No files found",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo")
return secureexec.Command("echo")
},
func(files []*models.File) {
assert.Len(t, files, 0)
@@ -329,7 +330,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
{
"Several files found",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command(
return secureexec.Command(
"echo",
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
)
@@ -422,7 +423,7 @@ func TestGitCommandStashDo(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.StashDo(1, "drop"))
@@ -435,7 +436,7 @@ func TestGitCommandStashSave(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.StashSave("A stash message"))
@@ -448,7 +449,7 @@ func TestGitCommandCommitAmend(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "--allow-empty"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
_, err := gitCmd.PrepareCommitAmendSubProcess().CombinedOutput()
@@ -548,7 +549,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
{
"Can't retrieve pushable count",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
return secureexec.Command("test")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
@@ -559,10 +560,10 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
"Can't retrieve pullable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return exec.Command("test")
return secureexec.Command("test")
}
return exec.Command("echo")
return secureexec.Command("echo")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
@@ -573,10 +574,10 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
"Retrieve pullable and pushable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return exec.Command("echo", "10")
return secureexec.Command("echo", "10")
}
return exec.Command("echo", "11")
return secureexec.Command("echo", "11")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "11", pushableCount)
@@ -601,7 +602,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.RenameCommit("test"))
@@ -614,7 +615,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
@@ -627,7 +628,7 @@ func TestGitCommandNewBranch(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.NewBranch("test", "master"))
@@ -652,7 +653,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -666,7 +667,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -690,7 +691,7 @@ func TestGitCommandMerge(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
@@ -699,45 +700,24 @@ func TestGitCommandMerge(t *testing.T) {
// TestGitCommandUsingGpg is a function.
func TestGitCommandUsingGpg(t *testing.T) {
type scenario struct {
testName string
getLocalGitConfig func(string) (string, error)
getGlobalGitConfig func(string) (string, error)
test func(bool)
testName string
getGitConfigValue func(string) (string, error)
test func(bool)
}
scenarios := []scenario{
{
"Option global and local config commit.gpgsign is not set",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "", nil
},
func(string) (string, error) { return "", nil },
func(gpgEnabled bool) {
assert.False(t, gpgEnabled)
},
},
{
"Option global config commit.gpgsign is not set, fallback on local config",
func(string) (string, error) {
return "", nil
},
func(string) (string, error) {
return "true", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
},
{
"Option commit.gpgsign is true",
func(string) (string, error) {
return "True", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
@@ -747,9 +727,6 @@ func TestGitCommandUsingGpg(t *testing.T) {
func(string) (string, error) {
return "ON", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
@@ -759,9 +736,6 @@ func TestGitCommandUsingGpg(t *testing.T) {
func(string) (string, error) {
return "YeS", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
@@ -771,9 +745,6 @@ func TestGitCommandUsingGpg(t *testing.T) {
func(string) (string, error) {
return "1", nil
},
func(string) (string, error) {
return "", nil
},
func(gpgEnabled bool) {
assert.True(t, gpgEnabled)
},
@@ -783,8 +754,7 @@ func TestGitCommandUsingGpg(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.getLocalGitConfig = s.getLocalGitConfig
gitCmd.getGitConfigValue = s.getGitConfigValue
s.test(gitCmd.usingGpg())
})
}
@@ -793,11 +763,11 @@ func TestGitCommandUsingGpg(t *testing.T) {
// TestGitCommandCommit is a function.
func TestGitCommandCommit(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
flags string
testName string
command func(string, ...string) *exec.Cmd
getGitConfigValue func(string) (string, error)
test func(*exec.Cmd, error)
flags string
}
scenarios := []scenario{
@@ -807,7 +777,7 @@ func TestGitCommandCommit(t *testing.T) {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(string) (string, error) {
return "true", nil
@@ -824,7 +794,7 @@ func TestGitCommandCommit(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "-m", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(string) (string, error) {
return "false", nil
@@ -841,7 +811,7 @@ func TestGitCommandCommit(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--no-verify", "-m", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(string) (string, error) {
return "false", nil
@@ -858,7 +828,7 @@ func TestGitCommandCommit(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "-m", "test"}, args)
return exec.Command("test")
return secureexec.Command("test")
},
func(string) (string, error) {
return "false", nil
@@ -874,7 +844,7 @@ func TestGitCommandCommit(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.getGitConfigValue = s.getGitConfigValue
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.Commit("test", s.flags))
})
@@ -884,10 +854,10 @@ func TestGitCommandCommit(t *testing.T) {
// TestGitCommandAmendHead is a function.
func TestGitCommandAmendHead(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
testName string
command func(string, ...string) *exec.Cmd
getGitConfigValue func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
@@ -897,7 +867,7 @@ func TestGitCommandAmendHead(t *testing.T) {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit --allow-empty"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(string) (string, error) {
return "true", nil
@@ -913,7 +883,7 @@ func TestGitCommandAmendHead(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(string) (string, error) {
return "false", nil
@@ -929,7 +899,7 @@ func TestGitCommandAmendHead(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args)
return exec.Command("test")
return secureexec.Command("test")
},
func(string) (string, error) {
return "false", nil
@@ -944,7 +914,7 @@ func TestGitCommandAmendHead(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.getGitConfigValue = s.getGitConfigValue
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.AmendHead())
})
@@ -954,20 +924,24 @@ 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
getGitConfigValue 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(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
false,
func(err error) {
@@ -975,12 +949,15 @@ func TestGitCommandPush(t *testing.T) {
},
},
{
"Push with force enabled",
"Push with force enabled, follow-tags on",
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)
return exec.Command("echo")
return secureexec.Command("echo")
},
true,
func(err error) {
@@ -988,11 +965,30 @@ func TestGitCommandPush(t *testing.T) {
},
},
{
"Push with an error occurring",
"Push with force disabled, follow-tags off",
func(string) (string, error) {
return "false", nil
},
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("echo")
},
false,
func(err error) {
assert.NoError(t, err)
},
},
{
"Push with an error occurring, follow-tags on",
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)
return exec.Command("test")
return secureexec.Command("test")
},
false,
func(err error) {
@@ -1005,6 +1001,7 @@ func TestGitCommandPush(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.getGitConfigValue = s.getGitConfigValue
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
return "\n"
})
@@ -1027,7 +1024,7 @@ func TestGitCommandCatFile(t *testing.T) {
assert.EqualValues(t, osCmd, cmd)
assert.EqualValues(t, []string{"test.txt"}, args)
return exec.Command("echo", "-n", "test")
return secureexec.Command("echo", "-n", "test")
}
o, err := gitCmd.CatFile("test.txt")
@@ -1042,7 +1039,7 @@ func TestGitCommandStageFile(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"add", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.StageFile("test.txt"))
@@ -1064,7 +1061,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"rm", "--cached", "--force", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -1077,7 +1074,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "HEAD", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -1113,7 +1110,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("test")
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1138,7 +1135,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("test")
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1161,7 +1158,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("test")
return secureexec.Command("test")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1187,7 +1184,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1213,7 +1210,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1240,7 +1237,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1267,7 +1264,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1294,7 +1291,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
return func(cmd string, args ...string) *exec.Cmd {
cmdsCalled = append(cmdsCalled, args)
return exec.Command("echo")
return secureexec.Command("echo")
}, &cmdsCalled
},
func(cmdsCalled *[][]string, err error) {
@@ -1340,7 +1337,7 @@ func TestGitCommandCheckout(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -1353,7 +1350,7 @@ func TestGitCommandCheckout(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -1377,12 +1374,24 @@ func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
}
_, err := gitCmd.GetBranchGraph("test")
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 secureexec.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 {
@@ -1400,7 +1409,7 @@ func TestGitCommandDiff(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
@@ -1416,7 +1425,7 @@ func TestGitCommandDiff(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
@@ -1432,7 +1441,7 @@ func TestGitCommandDiff(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
@@ -1448,7 +1457,7 @@ func TestGitCommandDiff(t *testing.T) {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
return secureexec.Command("echo")
},
&models.File{
Name: "test.txt",
@@ -1482,7 +1491,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
"says we are on the master branch if we are",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return exec.Command("echo", "master")
return secureexec.Command("echo", "master")
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
@@ -1498,10 +1507,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return exec.Command("echo", "* master")
return secureexec.Command("echo", "* master")
}
return nil
@@ -1520,10 +1529,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return exec.Command("echo", "* (HEAD detached at 123abcd)")
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
@@ -1538,7 +1547,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return exec.Command("test")
return secureexec.Command("test")
},
func(name string, displayname string, err error) {
assert.Error(t, err)
@@ -1576,7 +1585,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
assert.Equal(t, "test", string(content))
return exec.Command("echo", "done")
return secureexec.Command("echo", "done")
},
func(err error) {
assert.NoError(t, err)
@@ -1597,7 +1606,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
assert.Equal(t, "test", string(content))
return exec.Command("test")
return secureexec.Command("test")
},
func(err error) {
assert.Error(t, err)
@@ -1717,7 +1726,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
getLocalGitConfig func(string) (string, error)
getGitConfigValue func(string) (string, error)
commits []*models.Commit
commitIndex int
fileName string
@@ -1798,7 +1807,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.getLocalGitConfig = s.getLocalGitConfig
gitCmd.getGitConfigValue = s.getGitConfigValue
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
})
}
@@ -2087,3 +2096,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
getGitConfigValue func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.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 secureexec.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 secureexec.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 secureexec.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 secureexec.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 secureexec.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.getGitConfigValue = s.getGitConfigValue
s.test(gitCmd.EditFile(s.filename))
}
}
+56
View File
@@ -0,0 +1,56 @@
package commands
import (
"bytes"
"fmt"
"io/ioutil"
"os/exec"
"strings"
"syscall"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// including license from https://github.com/tcnksm/go-gitconfig because this file is an adaptation of that repo's code
// Copyright (c) 2014 tcnksm
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
func getGitConfigValue(key string) (string, error) {
gitArgs := []string{"config", "--get", "--null", key}
var stdout bytes.Buffer
cmd := secureexec.Command("git", gitArgs...)
cmd.Stdout = &stdout
cmd.Stderr = ioutil.Discard
err := cmd.Run()
if exitError, ok := err.(*exec.ExitError); ok {
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
if waitStatus.ExitStatus() == 1 {
return "", fmt.Errorf("the key `%s` is not found", key)
}
}
return "", err
}
return strings.TrimRight(stdout.String(), "\000"), nil
}
+2 -2
View File
@@ -185,9 +185,9 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
switch rebaseMode {
case "normal":
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case "interactive":
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
default:
return nil, nil
+8 -7
View File
@@ -6,6 +6,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
@@ -39,10 +40,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
return secureexec.Command("test")
}
return nil
},
@@ -59,10 +60,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
return secureexec.Command("echo", "blah")
}
return nil
},
@@ -79,10 +80,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
return secureexec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
return secureexec.Command("echo", "blah")
}
return nil
},
@@ -94,7 +95,7 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
return secureexec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
+1 -3
View File
@@ -1,7 +1,6 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
@@ -26,8 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
return c.getUnfilteredStashEntries()
}
unescaped := fmt.Sprintf("git stash list --name-only")
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
rawString, err := c.OSCommand.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}
+16 -42
View File
@@ -16,10 +16,10 @@ import (
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"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 +36,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: secureexec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
Getenv: os.Getenv,
}
}
@@ -235,31 +233,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 +444,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
}
+4 -155
View File
@@ -6,6 +6,7 @@ import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
@@ -70,7 +71,7 @@ func TestOSCommandOpenFile(t *testing.T) {
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
return secureexec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
@@ -81,7 +82,7 @@ func TestOSCommandOpenFile(t *testing.T) {
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"test"}, arg)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -92,7 +93,7 @@ func TestOSCommandOpenFile(t *testing.T) {
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"filename with spaces"}, arg)
return exec.Command("echo")
return secureexec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
@@ -109,158 +110,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()
+1 -1
View File
@@ -3,7 +3,7 @@ package oscommands
func getPlatform() *Platform {
return &Platform{
OS: "windows",
CatCmd: "type",
CatCmd: "cmd /c type",
Shell: "cmd",
ShellArg: "/c",
EscapedQuote: `\"`,
+2 -2
View File
@@ -149,7 +149,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == "rebasing" {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -169,7 +169,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == "rebasing" {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
+25 -5
View File
@@ -91,10 +91,29 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *models.Branch) error {
pullRequestURL, err := pr.getPullRequestURL(branch)
if err != nil {
return err
}
return pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
}
// CopyURL copies the pull request URL to the clipboard
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
pullRequestURL, err := pr.getPullRequestURL(branch)
if err != nil {
return err
}
return pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
}
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
}
repoURL := pr.GitCommand.GetRemoteURL()
@@ -108,14 +127,15 @@ func (pr *PullRequest) Create(branch *models.Branch) error {
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.UnsupportedGitService)
return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
}
repoInfo := getRepoInfoFromURL(repoURL)
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
pullRequestURL := fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
))
)
return pullRequestURL, nil
}
func getRepoInfoFromURL(url string) *RepoInformation {
+44 -33
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
@@ -46,98 +47,104 @@ 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")
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
return exec.Command("echo")
return secureexec.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")
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
return exec.Command("echo")
return secureexec.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")
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return exec.Command("echo")
return secureexec.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")
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return exec.Command("echo")
return secureexec.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 secureexec.Command("echo")
},
func(err error) {
test: func(err error) {
assert.Error(t, err)
},
},
@@ -155,6 +162,10 @@ func TestCreatePullRequest(t *testing.T) {
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.getGitConfigValue = func(path string) (string, error) {
assert.Equal(t, path, "remote.origin.url")
return s.remoteUrl, nil
}
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
+12 -5
View File
@@ -6,6 +6,13 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
)
const (
REBASE_MODE_NORMAL = "normal"
REBASE_MODE_INTERACTIVE = "interactive"
REBASE_MODE_REBASING = "rebasing"
REBASE_MODE_MERGING = "merging"
)
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
@@ -14,11 +21,11 @@ func (c *GitCommand) RebaseMode() (string, error) {
return "", err
}
if exists {
return "normal", nil
return REBASE_MODE_NORMAL, nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
if exists {
return "interactive", err
return REBASE_MODE_INTERACTIVE, err
} else {
return "", err
}
@@ -27,13 +34,13 @@ func (c *GitCommand) RebaseMode() (string, error) {
func (c *GitCommand) WorkingTreeState() string {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return "rebasing"
return REBASE_MODE_REBASING
}
merging, _ := c.IsInMergeState()
if merging {
return "merging"
return REBASE_MODE_MERGING
}
return "normal"
return REBASE_MODE_NORMAL
}
// IsInMergeState states whether we are still mid-merge
+7 -5
View File
@@ -13,10 +13,7 @@ func (c *GitCommand) usingGpg() bool {
return false
}
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
}
gpgsign := c.GetConfigValue("commit.gpgsign")
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
@@ -24,6 +21,11 @@ func (c *GitCommand) usingGpg() bool {
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
followTagsFlag := "--follow-tags"
if c.GetConfigValue("push.followTags") == "false" {
followTagsFlag = ""
}
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
@@ -34,7 +36,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, args s
setUpstreamArg = "--set-upstream " + upstream
}
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
}
+10 -5
View File
@@ -82,20 +82,25 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
}
func ConfigDir() string {
legacyConfigDirectory := configDirForVendor("jesseduffield")
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
return legacyConfigDirectory
}
configDirectory := configDirForVendor("")
return configDirectory
}
func configDirForVendor(vendor string) string {
envConfigDir := os.Getenv("CONFIG_DIR")
if envConfigDir != "" {
return envConfigDir
}
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := xdg.New("jesseduffield", "lazygit")
configDirs := xdg.New(vendor, "lazygit")
return configDirs.ConfigHome()
}
func findOrCreateConfigDir() (string, error) {
folder := ConfigDir()
err := os.MkdirAll(folder, 0755)
if err != nil {
return "", err
+35 -16
View File
@@ -4,6 +4,7 @@ type UserConfig struct {
Gui GuiConfig `yaml:"gui"`
Git GitConfig `yaml:"git"`
Update UpdateConfig `yaml:"update"`
Refresher RefresherConfig `yaml:"refresher"`
Reporting string `yaml:"reporting"`
SplashUpdatesIndex int `yaml:"splashUpdatesIndex"`
ConfirmOnQuit bool `yaml:"confirmOnQuit"`
@@ -14,19 +15,26 @@ type UserConfig struct {
DisableStartupPopups bool `yaml:"disableStartupPopups"`
CustomCommands []CustomCommand `yaml:"customCommands"`
Services map[string]string `yaml:"services"`
NotARepository string `yaml:"notARepository"`
}
type RefresherConfig struct {
RefreshInterval int `yaml:"refreshInterval"`
FetchInterval int `yaml:"fetchInterval"`
}
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 +57,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 +158,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 +179,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 +290,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,10 +308,14 @@ 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),
},
Refresher: RefresherConfig{
RefreshInterval: 10,
FetchInterval: 60,
},
Update: UpdateConfig{
Method: "prompt",
Days: 14,
@@ -366,8 +382,9 @@ func GetDefaultConfig() *UserConfig {
AppendNewline: "<tab>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
RecentRepos: "<enter>",
CheckForUpdate: "u",
RecentRepos: "<enter>",
AllBranchesLogGraph: "a",
},
Files: KeybindingFilesConfig{
CommitChanges: "c",
@@ -383,6 +400,7 @@ func GetDefaultConfig() *UserConfig {
Fetch: "f",
},
Branches: KeybindingBranchesConfig{
CopyPullRequestURL: "<c-y>",
CreatePullRequest: "o",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
@@ -438,5 +456,6 @@ func GetDefaultConfig() *UserConfig {
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
NotARepository: "prompt",
}
}
+71 -24
View File
@@ -1,6 +1,7 @@
package gui
import (
"sync"
"time"
"github.com/jesseduffield/gocui"
@@ -8,33 +9,68 @@ import (
)
type appStatus struct {
name string
message string
statusType string
duration int
id int
}
type statusManager struct {
statuses []appStatus
nextId int
mutex sync.Mutex
}
func (m *statusManager) removeStatus(name string) {
func (m *statusManager) removeStatus(id int) {
m.mutex.Lock()
defer m.mutex.Unlock()
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.name != name {
if status.id != id {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
}
func (m *statusManager) addWaitingStatus(name string) {
m.removeStatus(name)
func (m *statusManager) addWaitingStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId += 1
id := m.nextId
newStatus := appStatus{
name: name,
message: message,
statusType: "waiting",
duration: 0,
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
return id
}
func (m *statusManager) addToastStatus(message string) int {
m.mutex.Lock()
defer m.mutex.Unlock()
m.nextId++
id := m.nextId
newStatus := appStatus{
message: message,
statusType: "toast",
id: id,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
go func() {
time.Sleep(time.Second * 2)
m.removeStatus(id)
}()
return id
}
func (m *statusManager) getStatusString() string {
@@ -43,31 +79,42 @@ func (m *statusManager) getStatusString() string {
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader()
return topStatus.message + " " + utils.Loader()
}
return topStatus.name
return topStatus.message
}
func (gui *Gui) raiseToast(message string) {
gui.statusManager.addToastStatus(message)
gui.renderAppStatus()
}
func (gui *Gui) renderAppStatus() {
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
gui.renderString("appStatus", "")
return
}
gui.renderString("appStatus", appStatus)
}
})
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
go utils.Safe(func() {
gui.statusManager.addWaitingStatus(name)
id := gui.statusManager.addWaitingStatus(message)
defer func() {
gui.statusManager.removeStatus(name)
gui.statusManager.removeStatus(id)
}()
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
if appStatus == "" {
return
}
gui.renderString("appStatus", appStatus)
}
})
gui.renderAppStatus()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
+1
View File
@@ -182,6 +182,7 @@ func TestArrangeWindows(t *testing.T) {
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
})
+100 -45
View File
@@ -7,6 +7,8 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -99,8 +101,21 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
return nil
}
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
if err := pullRequest.CopyURL(branch); err != nil {
return gui.surfaceError(err)
}
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(v, gui.Tr.FetchWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
return err
}
go utils.Safe(func() {
@@ -194,21 +209,24 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.BranchName+":", "", func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.prompt(promptOpts{
title: gui.Tr.BranchName + ":",
findSuggestionsFunc: gui.findBranchNameSuggestions,
handleConfirm: func(response string) error {
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
onRefNotFound: func(ref string) error {
return gui.ask(askOpts{
title: gui.Tr.BranchNotFoundTitle,
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
})
return gui.ask(askOpts{
title: gui.Tr.BranchNotFoundTitle,
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
handleConfirm: func() error {
return gui.createNewBranchWithName(ref)
},
})
},
})
}},
)
}
func (gui *Gui) getCheckedOutBranch() *models.Branch {
@@ -265,7 +283,6 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
)
return gui.ask(askOpts{
title: title,
prompt: message,
handleConfirm: func() error {
@@ -302,7 +319,6 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
)
return gui.ask(askOpts{
title: gui.Tr.MergingTitle,
prompt: prompt,
handleConfirm: func() error {
@@ -344,7 +360,6 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
)
return gui.ask(askOpts{
title: gui.Tr.RebasingTitle,
prompt: prompt,
handleConfirm: func() error {
@@ -386,7 +401,7 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
},
)
go utils.Safe(func() {
_ = gui.createLoaderPanel(v, message)
_ = gui.createLoaderPanel(message)
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
@@ -418,17 +433,21 @@ 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 + ":",
initialContent: 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 +460,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 +493,62 @@ 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(sanitizedBranchName(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(sanitizedBranchName(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
}
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
// git's branch naming requirement.
func sanitizedBranchName(input string) string {
return strings.ReplaceAll(input, " ", "-")
}
+3 -3
View File
@@ -176,7 +176,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
}
}
if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); err != nil {
if err := gui.pushContext(gui.Contexts.PatchBuilding.Context); err != nil {
return err
}
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
@@ -192,7 +192,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return enterTheFile(selectedLineIdx)
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
},
})
}
@@ -215,5 +215,5 @@ func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, conte
return err
}
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
}
-40
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()
}
+25 -24
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
}
+111 -27
View File
@@ -12,6 +12,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -27,6 +28,8 @@ type createPopupPanelOpts struct {
// when handlersManageFocus is true, do not return from the confirmation context automatically. It's expected that the handlers will manage focus, whether that means switching to another context, or manually returning the context.
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
type askOpts struct {
@@ -35,13 +38,14 @@ type askOpts struct {
handleConfirm func() error
handleClose func() error
handlersManageFocus bool
findSuggestionsFunc func(string) []*types.Suggestion
}
func (gui *Gui) createLoaderPanel(currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
type promptOpts struct {
title string
initialContent string
handleConfirm func(string) error
findSuggestionsFunc func(string) []*types.Suggestion
}
func (gui *Gui) ask(opts askOpts) error {
@@ -51,20 +55,29 @@ func (gui *Gui) ask(opts askOpts) error {
handleConfirm: opts.handleConfirm,
handleClose: opts.handleClose,
handlersManageFocus: opts.handlersManageFocus,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
func (gui *Gui) prompt(title string, initialContent string, handleConfirm func(string) error) error {
func (gui *Gui) prompt(opts promptOpts) error {
return gui.createPopupPanel(createPopupPanelOpts{
title: title,
prompt: initialContent,
title: opts.title,
prompt: opts.initialContent,
editable: true,
handleConfirmPrompt: handleConfirm,
handleConfirmPrompt: opts.handleConfirm,
findSuggestionsFunc: opts.findSuggestionsFunc,
})
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) createLoaderPanel(prompt string) error {
return gui.createPopupPanel(createPopupPanelOpts{
prompt: prompt,
hasLoader: true,
})
}
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error {
return func() error {
if function != nil {
if err := function(); err != nil {
return err
@@ -79,10 +92,10 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f
}
}
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error, getResponse func() string) func() error {
return func() error {
if function != nil {
if err := function(v.Buffer()); err != nil {
if err := function(getResponse()); err != nil {
return gui.surfaceError(err)
}
}
@@ -117,6 +130,9 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
}
gui.deleteConfirmationView()
_, _ = gui.g.SetViewOnBottom("suggestions")
return nil
}
@@ -156,11 +172,11 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
height/2 + panelHeight/2
}
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (*gocui.View, error) {
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
confirmationView.HasLoader = hasLoader
@@ -171,8 +187,24 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
}
gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil {
suggestionsViewHeight := 11
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return nil, err
}
suggestionsView.Wrap = true
suggestionsView.FgColor = theme.GocuiDefaultTextColor
}
gui.setSuggestions([]*types.Suggestion{})
_, _ = gui.g.SetViewOnTop("suggestions")
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchContext(gui.Contexts.Confirmation.Context)
return gui.pushContext(gui.Contexts.Confirmation.Context)
})
return confirmationView, nil
}
@@ -183,11 +215,12 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
if view, _ := g.View("confirmation"); view != nil {
gui.deleteConfirmationView()
}
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader)
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
if err != nil {
return err
}
confirmationView.Editable = opts.editable
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
if opts.editable {
go utils.Safe(func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
@@ -200,6 +233,7 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
}
gui.renderString("confirmation", opts.prompt)
return gui.setKeyBindings(opts)
})
return nil
@@ -215,22 +249,72 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
)
gui.renderString("options", actions)
var onConfirm func(*gocui.Gui, *gocui.View) error
var onConfirm func() error
if opts.handleConfirmPrompt != nil {
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt)
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getConfirmationView().Buffer() })
} else {
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone, onConfirm); err != nil {
return err
}
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone, onConfirm); err != nil {
return err
type confirmationKeybinding struct {
viewName string
key interface{}
handler func() error
}
return gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
keybindingConfig := gui.Config.GetUserConfig().Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
confirmationKeybindings := []confirmationKeybinding{
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.Confirm),
handler: onConfirm,
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
handler: onConfirm,
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.Return),
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
},
{
viewName: "confirmation",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Suggestions.Context) },
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.Confirm),
handler: onSuggestionConfirm,
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
handler: onSuggestionConfirm,
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.Return),
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
},
{
viewName: "suggestions",
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
handler: func() error { return gui.replaceContext(gui.Contexts.Confirmation.Context) },
},
}
for _, binding := range confirmationKeybindings {
if err := gui.g.SetKeybinding(binding.viewName, nil, binding.key, gocui.ModNone, gui.wrappedHandler(binding.handler)); err != nil {
return err
}
}
return nil
}
func (gui *Gui) createErrorPanel(message string) error {
+49 -25
View File
@@ -35,6 +35,7 @@ const (
SEARCH_CONTEXT_KEY = "search"
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
SUBMODULES_CONTEXT_KEY = "submodules"
SUGGESTIONS_CONTEXT_KEY = "suggestions"
)
var allContextKeys = []string{
@@ -59,6 +60,7 @@ var allContextKeys = []string{
SEARCH_CONTEXT_KEY,
COMMIT_MESSAGE_CONTEXT_KEY,
SUBMODULES_CONTEXT_KEY,
SUGGESTIONS_CONTEXT_KEY,
}
type SimpleContextNode struct {
@@ -91,6 +93,7 @@ type ContextTree struct {
Confirmation SimpleContextNode
CommitMessage SimpleContextNode
Search SimpleContextNode
Suggestions SimpleContextNode
}
func (gui *Gui) allContexts() []Context {
@@ -115,6 +118,7 @@ func (gui *Gui) allContexts() []Context {
gui.Contexts.Merging.Context,
gui.Contexts.PatchBuilding.Context,
gui.Contexts.SubCommits.Context,
gui.Contexts.Suggestions.Context,
}
}
@@ -280,9 +284,7 @@ func (gui *Gui) contextTree() ContextTree {
},
Merging: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error {
return gui.refreshMergePanel()
},
OnFocus: gui.refreshMergePanel,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
@@ -291,7 +293,7 @@ func (gui *Gui) contextTree() ContextTree {
},
Credentials: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCredentialsViewFocused() },
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
@@ -305,9 +307,12 @@ func (gui *Gui) contextTree() ContextTree {
Key: CONFIRMATION_CONTEXT_KEY,
},
},
Suggestions: SimpleContextNode{
Context: gui.suggestionsListContext(),
},
CommitMessage: SimpleContextNode{
Context: BasicContext{
OnFocus: func() error { return gui.handleCommitMessageFocused() },
OnFocus: gui.handleCommitMessageFocused,
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
@@ -402,7 +407,24 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
return ""
}
func (gui *Gui) switchContext(c Context) error {
// use replaceContext when you don't want to return to the original context upon
// hitting escape: you want to go that context's parent instead.
func (gui *Gui) replaceContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
if len(gui.State.ContextStack) == 0 {
gui.State.ContextStack = []Context{c}
} else {
// replace the last item with the given item
gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c)
}
return gui.activateContext(c)
})
return nil
}
func (gui *Gui) pushContext(c Context) error {
gui.g.Update(func(*gocui.Gui) error {
// push onto stack
// if we are switching to a side context, remove all other contexts in the stack
@@ -426,11 +448,11 @@ func (gui *Gui) switchContext(c Context) error {
return nil
}
// switchContextToView is to be used when you don't know which context you
// pushContextWithView is to be used when you don't know which context you
// want to switch to: you only know the view that you want to switch to. It will
// look up the context currently active for that view and switch to that context
func (gui *Gui) switchContextToView(viewName string) error {
return gui.switchContext(gui.State.ViewContextMap[viewName])
func (gui *Gui) pushContextWithView(viewName string) error {
return gui.pushContext(gui.State.ViewContextMap[viewName])
}
func (gui *Gui) returnFromContext() error {
@@ -513,7 +535,7 @@ func (gui *Gui) activateContext(c Context) error {
if viewName == "main" {
gui.changeMainViewsContext(c.GetKey())
} else {
gui.changeMainViewsContext("normal")
gui.changeMainViewsContext(MAIN_NORMAL_CONTEXT_KEY)
}
gui.setViewTabForContext(c)
@@ -556,13 +578,14 @@ func (gui *Gui) activateContext(c Context) error {
return nil
}
func (gui *Gui) renderContextStack() string {
result := ""
for _, context := range gui.State.ContextStack {
result += context.GetKey() + "\n"
}
return result
}
// currently unused
// func (gui *Gui) renderContextStack() string {
// result := ""
// for _, context := range gui.State.ContextStack {
// result += context.GetKey() + "\n"
// }
// return result
// }
func (gui *Gui) currentContext() Context {
if len(gui.State.ContextStack) == 0 {
@@ -755,16 +778,17 @@ func (gui *Gui) rerenderView(viewName string) error {
return context.HandleRender()
}
func (gui *Gui) getCurrentSideView() *gocui.View {
currentSideContext := gui.currentSideContext()
if currentSideContext == nil {
return nil
}
// currently unused
// func (gui *Gui) getCurrentSideView() *gocui.View {
// currentSideContext := gui.currentSideContext()
// if currentSideContext == nil {
// return nil
// }
view, _ := gui.g.View(currentSideContext.GetViewName())
// view, _ := gui.g.View(currentSideContext.GetViewName())
return view
}
// return view
// }
func (gui *Gui) getSideContextSelectedItemId() string {
currentSideContext := gui.currentSideContext()
+6 -5
View File
@@ -14,18 +14,19 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
gui.credentials = make(chan string)
gui.g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
switch passOrUname {
case "username":
credentialsView.Title = gui.Tr.CredentialsUsername
credentialsView.Mask = 0
} else if passOrUname == "password" {
case "password":
credentialsView.Title = gui.Tr.CredentialsPassword
credentialsView.Mask = '*'
} else {
default:
credentialsView.Title = gui.Tr.CredentialsPassphrase
credentialsView.Mask = '*'
}
if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Credentials.Context); err != nil {
return err
}
@@ -77,7 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
errMessage = gui.Tr.PassUnameWrong
}
// we are not logging this error because it may contain a password or a passphrase
gui.createErrorPanel(errMessage)
_ = gui.createErrorPanel(errMessage)
} else {
_ = gui.closeConfirmationPrompt(false)
}
+5 -5
View File
@@ -98,15 +98,15 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
return gui.surfaceError(err)
}
return gui.prompt(
title,
initialValue,
func(str string) error {
return gui.prompt(promptOpts{
title: title,
initialContent: initialValue,
handleConfirm: func(str string) error {
promptResponses[idx] = str
return wrappedF()
},
)
})
}
case "menu":
f = func() error {
+6 -3
View File
@@ -128,9 +128,12 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
{
displayString: gui.Tr.LcEnterRefToDiff,
onPress: func() error {
return gui.prompt(gui.Tr.LcEnteRefName, "", func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.prompt(promptOpts{
title: gui.Tr.LcEnteRefName,
handleConfirm: func(response string) error {
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
})
},
},
+1 -12
View File
@@ -2,19 +2,8 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (gui *Gui) submoduleFromFile(file *models.File) *models.SubmoduleConfig {
for _, config := range gui.State.Submodules {
if config.Name == file.Name {
return config
}
}
return nil
}
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedFile()
if file == nil {
@@ -31,7 +20,7 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
{
displayString: gui.Tr.LcSubmoduleStashAndReset,
onPress: func() error {
return gui.resetSubmodule(submodule)
return gui.handleResetSubmodule(submodule)
},
},
}
+80
View File
@@ -0,0 +1,80 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyTab
}
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == newlineKey:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
}
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
if gui.findSuggestions != nil {
input := v.Buffer()
suggestions := gui.findSuggestions(input)
gui.setSuggestions(suggestions)
}
}
+1 -16
View File
@@ -28,21 +28,6 @@ func NewFileWatcher(log *logrus.Entry) *fileWatcher {
return &fileWatcher{
Disabled: true,
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error(err)
return &fileWatcher{
Disabled: true,
}
}
return &fileWatcher{
Watcher: watcher,
Log: log,
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
}
}
func (w *fileWatcher) watchingFilename(filename string) bool {
@@ -132,7 +117,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
}
// watch for errors
+56 -33
View File
@@ -152,7 +152,7 @@ func (gui *Gui) trackedFiles() []*models.File {
return result
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
func (gui *Gui) stageSelectedFile() error {
file := gui.getSelectedFile()
if file == nil {
return nil
@@ -183,7 +183,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
gui.switchContext(gui.Contexts.Staging.Context)
_ = gui.pushContext(gui.Contexts.Staging.Context)
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
}
@@ -284,7 +284,7 @@ func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
gui.renderStringSync("commitMessage", skipHookPreifx)
_ = gui.renderStringSync("commitMessage", skipHookPreifx)
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
@@ -301,11 +301,27 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
return &cfg
}
func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
err := gui.GitCommand.StageAll()
if err != nil {
return err
}
return gui.refreshFilesAndSubmodules()
}
return nil
}
func (gui *Gui) handleCommitPress() error {
if err := gui.prepareFilesForCommit(); err != nil {
return gui.surfaceError(err)
}
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitPress()
})
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
}
commitMessageView := gui.getCommitMessageView()
@@ -325,7 +341,7 @@ func (gui *Gui) handleCommitPress() error {
}
gui.g.Update(func(g *gocui.Gui) error {
if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
if err := gui.pushContext(gui.Contexts.CommitMessage.Context); err != nil {
return err
}
@@ -354,9 +370,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
func (gui *Gui) handleAmendCommitPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleAmendCommitPress()
})
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
}
if len(gui.State.Commits) == 0 {
@@ -386,9 +400,7 @@ func (gui *Gui) handleAmendCommitPress() error {
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress() error {
if len(gui.stagedFiles()) == 0 {
return gui.promptToStageAllAndRetry(func() error {
return gui.handleCommitEditorPress()
})
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
}
gui.PrepareSubProcess("git commit")
@@ -405,7 +417,7 @@ func (gui *Gui) PrepareSubProcess(command string) {
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
_, err := gui.runSyncOrAsyncCommand(gui.GitCommand.EditFile(filename))
return err
}
@@ -484,15 +496,19 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
}
}
return gui.prompt(gui.Tr.EnterUpstream, "origin/"+currentBranch.Name, func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin/" + currentBranch.Name,
handleConfirm: func(upstream string) error {
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(errorMessage)
}
return gui.createErrorPanel(errorMessage)
}
return gui.pullFiles(PullFilesOptions{})
return gui.pullFiles(PullFilesOptions{})
},
})
}
@@ -505,13 +521,13 @@ type PullFilesOptions struct {
}
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
if err := gui.createLoaderPanel(gui.g.CurrentView(), gui.Tr.PullWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.PullWait); err != nil {
return err
}
mode := gui.Config.GetUserConfig().Git.Pull.Mode
go utils.Safe(func() { gui.pullWithMode(mode, opts) })
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
return nil
}
@@ -548,7 +564,7 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
}
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
if err := gui.createLoaderPanel(v, gui.Tr.PushWait); err != nil {
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
return err
}
go utils.Safe(func() {
@@ -557,10 +573,10 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
if forcePushDisabled {
gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
return
}
gui.ask(askOpts{
_ = gui.ask(askOpts{
title: gui.Tr.ForcePush,
prompt: gui.Tr.ForcePushPrompt,
handleConfirm: func() error {
@@ -598,8 +614,12 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
if gui.GitCommand.PushToCurrent {
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
} else {
return gui.prompt(gui.Tr.EnterUpstream, "origin "+currentBranch.Name, func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
return gui.prompt(promptOpts{
title: gui.Tr.EnterUpstream,
initialContent: "origin " + currentBranch.Name,
handleConfirm: func(response string) error {
return gui.pushWithForceFlag(v, false, response, "")
},
})
}
} else if currentBranch.Pullables == "0" {
@@ -630,7 +650,7 @@ func (gui *Gui) handleSwitchToMerge() error {
return gui.createErrorPanel(gui.Tr.FileNoMergeCons)
}
return gui.switchContext(gui.Contexts.Merging.Context)
return gui.pushContext(gui.Contexts.Merging.Context)
}
func (gui *Gui) openFile(filename string) error {
@@ -650,9 +670,12 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.prompt(gui.Tr.CustomCommand, "", func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
handleConfirm: func(command string) error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
},
})
}
+3 -5
View File
@@ -3,11 +3,9 @@ package gui
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.State.Modes.Filtering.Active() {
err := gui.ask(askOpts{
title: gui.Tr.MustExitFilterModeTitle,
prompt: gui.Tr.MustExitFilterModePrompt,
handleConfirm: func() error {
return gui.exitFilterMode()
},
title: gui.Tr.MustExitFilterModeTitle,
prompt: gui.Tr.MustExitFilterModePrompt,
handleConfirm: gui.exitFilterMode,
})
return false, err
+6 -3
View File
@@ -41,9 +41,12 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcFilterPathOption,
onPress: func() error {
return gui.prompt(gui.Tr.LcEnterFileName, "", func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
return gui.prompt(promptOpts{
title: gui.Tr.LcEnterFileName,
handleConfirm: func(response string) error {
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
return gui.Errors.ErrRestart
},
})
},
})
+8 -4
View File
@@ -52,10 +52,14 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
startHandler := func(branchType string) func() error {
return func() error {
title := utils.ResolvePlaceholderString(gui.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType})
return gui.prompt(title, "", func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
return gui.prompt(promptOpts{
title: title,
handleConfirm: func(name string) error {
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
},
})
}
}
+12 -3
View File
@@ -1,6 +1,7 @@
package gui
import (
"fmt"
"math"
"strings"
@@ -175,10 +176,10 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
err = gui.GitCommand.Fetch(fetchOpts)
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
gui.createErrorPanel(gui.Tr.PassUnameWrong)
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
}
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
_ = gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
return err
}
@@ -191,5 +192,13 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
return nil
}
return gui.OSCommand.CopyToClipboard(itemId)
if err := gui.OSCommand.CopyToClipboard(itemId); err != nil {
return gui.surfaceError(err)
}
truncatedItemId := utils.TruncateWithEllipsis(strings.Replace(itemId, "\n", " ", -1), 50)
gui.raiseToast(fmt.Sprintf("'%s' %s", truncatedItemId, gui.Tr.LcCopiedToClipboard))
return nil
}
+20 -3
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,
@@ -480,7 +495,7 @@ func (gui *Gui) Run() error {
go utils.Safe(gui.startBackgroundFetch)
}
gui.goEvery(time.Second*10, gui.stopChan, gui.refreshFilesAndSubmodules)
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.RefreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
@@ -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)
@@ -627,8 +643,9 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
func (gui *Gui) startBackgroundFetch() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
userConfig := gui.Config.GetUserConfig()
if !isNew {
time.After(60 * time.Second)
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
}
err := gui.fetch(false)
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
@@ -637,7 +654,7 @@ func (gui *Gui) startBackgroundFetch() {
prompt: gui.Tr.NoAutomaticGitFetchBody,
})
} else {
gui.goEvery(time.Second*60, gui.stopChan, func() error {
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
err := gui.fetch(false)
return err
})
+2 -2
View File
@@ -6,7 +6,6 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
@@ -14,6 +13,7 @@ import (
"github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
@@ -250,7 +250,7 @@ func Test(t *testing.T) {
func createFixture(testPath, actualDir string) error {
osCommand := oscommands.NewDummyOSCommand()
bashScriptPath := filepath.Join(testPath, "setup.sh")
cmd := exec.Command("bash", bashScriptPath, actualDir)
cmd := secureexec.Command("bash", bashScriptPath, actualDir)
if err := osCommand.RunExecutable(cmd); err != nil {
return err
+32
View File
@@ -22,6 +22,7 @@ type Binding struct {
Description string
Alternative string
Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet
OpensMenu bool
}
// GetDisplayStrings returns the display string of a file
@@ -268,12 +269,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Universal.CreateRebaseOptionsMenu),
Handler: gui.wrappedHandler(gui.handleCreateRebaseOptionsMenu),
Description: gui.Tr.ViewMergeRebaseOptions,
OpensMenu: true,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.CreatePatchOptionsMenu),
Handler: gui.handleCreatePatchOptionsMenu,
Description: gui.Tr.ViewPatchOptions,
OpensMenu: true,
},
{
ViewName: "",
@@ -298,6 +301,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Universal.OptionMenu),
Handler: gui.handleCreateOptionsMenu,
Description: gui.Tr.LcOpenMenu,
OpensMenu: true,
},
{
ViewName: "",
@@ -359,6 +363,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},
@@ -400,6 +410,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Universal.Remove),
Handler: gui.handleCreateDiscardMenu,
Description: gui.Tr.LcViewDiscardOptions,
OpensMenu: true,
},
{
ViewName: "files",
@@ -442,6 +453,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Files.ViewStashOptions),
Handler: gui.handleCreateStashMenu,
Description: gui.Tr.LcViewStashOptions,
OpensMenu: true,
},
{
ViewName: "files",
@@ -456,6 +468,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Files.ViewResetOptions),
Handler: gui.handleCreateResetMenu,
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "files",
@@ -490,6 +503,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.handleCreateResetToUpstreamMenu,
Description: gui.Tr.LcViewResetToUpstreamOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -505,6 +519,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},
@@ -553,6 +574,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Branches.ViewGitFlowOptions),
Handler: gui.handleCreateGitFlowMenu,
Description: gui.Tr.LcGitFlowOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -567,6 +589,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.handleCreateResetToBranchMenu,
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -623,6 +646,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.handleCreateResetToTagMenu,
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -644,6 +668,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.handleCreateResetToRemoteBranchMenu,
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -848,6 +873,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.handleCreateReflogResetMenu,
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "commits",
@@ -903,6 +929,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Commits.ViewResetOptions),
Handler: gui.wrappedHandler(gui.handleCreateSubCommitResetMenu),
Description: gui.Tr.LcViewResetOptions,
OpensMenu: true,
},
{
ViewName: "branches",
@@ -1046,18 +1073,21 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Universal.FilteringMenu),
Handler: gui.handleCreateFilteringMenuPanel,
Description: gui.Tr.LcOpenScopingMenu,
OpensMenu: true,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.DiffingMenu),
Handler: gui.handleCreateDiffingMenuPanel,
Description: gui.Tr.LcOpenDiffingMenu,
OpensMenu: true,
},
{
ViewName: "",
Key: gui.getKey(config.Universal.DiffingMenuAlt),
Handler: gui.handleCreateDiffingMenuPanel,
Description: gui.Tr.LcOpenDiffingMenu,
OpensMenu: true,
},
{
ViewName: "secondary",
@@ -1605,6 +1635,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Universal.Remove),
Handler: gui.forSubmodule(gui.handleResetRemoveSubmodule),
Description: gui.Tr.LcViewResetAndRemoveOptions,
OpensMenu: true,
},
{
ViewName: "files",
@@ -1640,6 +1671,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Key: gui.getKey(config.Submodules.BulkMenu),
Handler: gui.wrappedHandler(gui.handleBulkSubmoduleActionsMenu),
Description: gui.Tr.LcViewBulkSubmoduleOptions,
OpensMenu: true,
},
}
+18 -25
View File
@@ -34,7 +34,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.NotEnoughSpace
@@ -101,7 +101,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
v, err := setViewFromDimensions("main", "main", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.DiffTitle
@@ -112,7 +112,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
secondaryView.Title = gui.Tr.DiffTitle
@@ -124,7 +124,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
hiddenViewOffset := 9999
if v, err := setViewFromDimensions("status", "status", true); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Title = gui.Tr.StatusTitle
@@ -133,7 +133,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
filesView, err := setViewFromDimensions("files", "files", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
filesView.Highlight = true
@@ -144,7 +144,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
branchesView, err := setViewFromDimensions("branches", "branches", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
branchesView.Title = gui.Tr.BranchesTitle
@@ -154,7 +154,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitFilesView.Title = gui.Tr.CommitFiles
@@ -165,7 +165,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
commitsView, err := setViewFromDimensions("commits", "commits", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
commitsView.Title = gui.Tr.CommitsTitle
@@ -175,7 +175,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
stashView, err := setViewFromDimensions("stash", "stash", true)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
stashView.Title = gui.Tr.StashTitle
@@ -186,7 +186,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("commitMessage")
@@ -200,7 +200,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
_, _ = g.SetViewOnBottom("credentials")
@@ -211,7 +211,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if v, err := setViewFromDimensions("options", "options", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
v.Frame = false
@@ -225,7 +225,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
// this view takes up one character. Its only purpose is to show the slash when searching
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
@@ -236,7 +236,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
@@ -247,7 +247,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
appStatusView.BgColor = gocui.ColorDefault
@@ -258,7 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
informationView, err := setViewFromDimensions("information", "information", false)
if err != nil {
if err.Error() != "unknown view" {
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
informationView.BgColor = gocui.ColorDefault
@@ -277,7 +277,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
initialContext = gui.Contexts.BranchCommits.Context
}
if err := gui.switchContext(initialContext); err != nil {
if err := gui.pushContext(initialContext); err != nil {
return err
}
}
@@ -351,7 +351,7 @@ func (gui *Gui) onInitialViewsCreation() error {
}
gui.g.Mutexes.ViewsMutex.Unlock()
if err := gui.switchContext(gui.defaultSideContext()); err != nil {
if err := gui.pushContext(gui.defaultSideContext()); err != nil {
return err
}
@@ -368,10 +368,3 @@ func (gui *Gui) onInitialViewsCreation() error {
return gui.loadNewRepo()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
+20 -2
View File
@@ -219,7 +219,7 @@ func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
newSelectedLineIdx := v.SelectedLineIdx()
// we need to focus the view
if err := lc.Gui.switchContext(lc); err != nil {
if err := lc.Gui.pushContext(lc); err != nil {
return err
}
@@ -248,7 +248,7 @@ func (gui *Gui) menuListContext() *ListContext {
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: func() error { return gui.onMenuPress() },
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
@@ -483,6 +483,23 @@ func (gui *Gui) submodulesListContext() *ListContext {
}
}
func (gui *Gui) suggestionsListContext() *ListContext {
return &ListContext{
ViewName: "suggestions",
WindowName: "suggestions",
ContextKey: SUGGESTIONS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Suggestions) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
GetDisplayStrings: func() [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
gui.menuListContext(),
@@ -497,6 +514,7 @@ func (gui *Gui) getListContexts() []*ListContext {
gui.stashListContext(),
gui.commitFilesListContext(),
gui.submodulesListContext(),
gui.suggestionsListContext(),
}
}
+8 -11
View File
@@ -14,11 +14,6 @@ type viewUpdateOpts struct {
task updateTask
}
type coordinates struct {
x int
y int
}
type refreshMainOpts struct {
main *viewUpdateOpts
secondary *viewUpdateOpts
@@ -91,9 +86,10 @@ func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
return &runPtyTask{cmd: cmd}
}
func (gui *Gui) createRunPtyTaskWithPrefix(cmd *exec.Cmd, prefix string) *runPtyTask {
return &runPtyTask{cmd: cmd, prefix: prefix}
}
// currently unused
// func (gui *Gui) createRunPtyTaskWithPrefix(cmd *exec.Cmd, prefix string) *runPtyTask {
// return &runPtyTask{cmd: cmd, prefix: prefix}
// }
type runFunctionTask struct {
f func(chan struct{}) error
@@ -103,9 +99,10 @@ func (t *runFunctionTask) GetKind() int {
return RUN_FUNCTION
}
func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
return &runFunctionTask{f: f}
}
// currently unused
// func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
// return &runFunctionTask{f: f}
// }
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
switch task.GetKind() {
+1 -1
View File
@@ -88,7 +88,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
gui.State.Panels.Menu.SelectedLineIdx = 0
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchContext(gui.Contexts.Menu.Context)
return gui.pushContext(gui.Contexts.Menu.Context)
})
return nil
}
+16 -19
View File
@@ -50,9 +50,9 @@ func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top b
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
if len(conflicts) == 0 {
return content, nil
return content
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
@@ -71,7 +71,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
return outputBuffer.String()
}
func (gui *Gui) takeOverScrolling() {
@@ -142,7 +142,7 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
func (gui *Gui) pushFileSnapshot() error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
@@ -175,7 +175,7 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
if err := gui.pushFileSnapshot(g); err != nil {
if err := gui.pushFileSnapshot(); err != nil {
return err
}
@@ -201,7 +201,7 @@ func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
if err := gui.pushFileSnapshot(g); err != nil {
if err := gui.pushFileSnapshot(); err != nil {
return err
}
err := gui.resolveConflict(conflict, "both")
@@ -213,7 +213,7 @@ func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
cat, err := gui.catSelectedFile()
if err != nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -233,12 +233,9 @@ func (gui *Gui) refreshMergePanel() error {
}
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err != nil {
return err
}
content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err := gui.scrollToConflict(gui.g); err != nil {
if err := gui.scrollToConflict(); err != nil {
return err
}
@@ -251,7 +248,7 @@ func (gui *Gui) refreshMergePanel() error {
})
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
func (gui *Gui) catSelectedFile() (string, error) {
item := gui.getSelectedFile()
if item == nil {
return "", errors.New(gui.Tr.NoFilesDisplay)
@@ -269,7 +266,7 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
return cat, nil
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
func (gui *Gui) scrollToConflict() error {
if gui.State.Panels.Merging.UserScrolling {
return nil
}
@@ -312,13 +309,13 @@ func (gui *Gui) handleEscapeMerge() error {
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
}
return nil
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
if err := gui.stageSelectedFile(); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
@@ -326,7 +323,7 @@ func (gui *Gui) handleCompleteMerge() error {
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == "normal" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
@@ -345,14 +342,14 @@ func (gui *Gui) promptToContinue() error {
prompt: gui.Tr.ConflictsResolved,
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.switchContext(gui.Contexts.Files.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Files.Context); err != nil {
return err
}
return gui.genericMergeCommand("continue")
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
},
})
}
+13 -1
View File
@@ -3,6 +3,7 @@ package gui
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -33,6 +34,17 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
return append(bindingsPanel, bindingsGlobal...)
}
func (gui *Gui) displayDescription(binding *Binding) string {
commandColor := color.New(color.FgCyan)
menuColor := color.New(color.FgMagenta)
if binding.OpensMenu {
return menuColor.Sprintf("%s...", binding.Description)
}
return commandColor.Sprint(binding.Description)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
@@ -41,7 +53,7 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
for i, binding := range bindings {
binding := binding // note to self, never close over loop variables
menuItems[i] = &menuItem{
displayStrings: []string{GetKeyDisplay(binding.Key), binding.Description},
displayStrings: []string{GetKeyDisplay(binding.Key), gui.displayDescription(binding)},
onPress: func() error {
if binding.Key == nil {
return nil
+1 -1
View File
@@ -110,7 +110,7 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
}
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
return gui.switchContext(gui.Contexts.CommitFiles.Context)
return gui.pushContext(gui.Contexts.CommitFiles.Context)
} else {
// need to re-focus in case the secondary view should now be hidden
return gui.currentContext().HandleFocus()
+4 -3
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
@@ -26,7 +27,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
},
}
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == "normal" {
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == commands.REBASE_MODE_NORMAL {
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
@@ -74,7 +75,7 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != "normal" {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError)
}
return true, nil
@@ -179,7 +180,7 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
if err := gui.switchContext(gui.Contexts.CommitFiles.Context); err != nil {
if err := gui.pushContext(gui.Contexts.CommitFiles.Context); err != nil {
return err
}
}
+19
View File
@@ -0,0 +1,19 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func GetSuggestionListDisplayStrings(suggestions []*types.Suggestion) [][]string {
lines := make([][]string, len(suggestions))
for i := range suggestions {
lines[i] = getSuggestionDisplayStrings(suggestions[i])
}
return lines
}
func getSuggestionDisplayStrings(suggestion *types.Suggestion) []string {
return []string{suggestion.Label}
}
+1 -1
View File
@@ -40,7 +40,7 @@ func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack
return gui.switchContext(parentContext)
return gui.pushContext(parentContext)
}
for _, mode := range gui.modeStatuses() {
+7 -5
View File
@@ -3,12 +3,14 @@ package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{"continue", "abort"}
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
options = append(options, "skip")
}
@@ -25,7 +27,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
}
var title string
if gui.GitCommand.WorkingTreeState() == "merging" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle
} else {
title = gui.Tr.RebaseOptionsTitle
@@ -37,7 +39,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.GitCommand.WorkingTreeState()
if status != "merging" && status != "rebasing" {
if status != commands.REBASE_MODE_MERGING && status != commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing)
}
@@ -45,7 +47,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == "merging" && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
@@ -81,7 +83,7 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
prompt: gui.Tr.FoundConflicts,
handlersManageFocus: true,
handleConfirm: func() error {
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
},
handleClose: func() error {
if err := gui.returnFromContext(); err != nil {
+14
View File
@@ -32,6 +32,20 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
return gui.createMenu(gui.Tr.RecentRepos, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleShowAllBranchLogs() error {
cmd := gui.OSCommand.ExecutableFromString(
gui.Config.GetUserConfig().Git.AllBranchesLogCmd,
)
task := gui.createRunPtyTask(cmd)
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "Log",
task: task,
},
})
}
func (gui *Gui) dispatchSwitchToRepo(path string) error {
env.UnsetGitDirEnvs()
if err := os.Chdir(path); err != nil {
+1 -1
View File
@@ -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 {
+46 -31
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}})
},
})
},
})
}
+1 -1
View File
@@ -17,7 +17,7 @@ func (gui *Gui) resetToRef(ref string, strength string, options oscommands.RunCo
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.switchContext(gui.Contexts.BranchCommits.Context); err != nil {
if err := gui.pushContext(gui.Contexts.BranchCommits.Context); err != nil {
return err
}
+1 -1
View File
@@ -14,7 +14,7 @@ func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
gui.renderString("search", "")
if err := gui.switchContext(gui.Contexts.Search.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Search.Context); err != nil {
return err
}
+3 -3
View File
@@ -25,7 +25,7 @@ func (gui *Gui) nextSideWindow() error {
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
return gui.pushContextWithView(viewName)
}
func (gui *Gui) previousSideWindow() error {
@@ -51,11 +51,11 @@ func (gui *Gui) previousSideWindow() error {
viewName := gui.getViewNameForWindow(newWindow)
return gui.switchContextToView(viewName)
return gui.pushContextWithView(viewName)
}
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
return gui.switchContextToView(sideViewName)
return gui.pushContextWithView(sideViewName)
}
}
+5 -7
View File
@@ -18,10 +18,8 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
secondaryFocused := false
if forceSecondaryFocused {
secondaryFocused = true
} else {
if state != nil {
secondaryFocused = state.SecondaryFocused
}
} else if state != nil {
secondaryFocused = state.SecondaryFocused
}
if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) {
@@ -87,7 +85,7 @@ func (gui *Gui) handleTogglePanel() error {
func (gui *Gui) handleStagingEscape() error {
gui.escapeLineByLinePanel()
return gui.switchContext(gui.Contexts.Files.Context)
return gui.pushContext(gui.Contexts.Files.Context)
}
func (gui *Gui) handleToggleStagedSelection() error {
@@ -110,7 +108,7 @@ func (gui *Gui) handleResetSelection() error {
handlersManageFocus: true,
handleConfirm: func() error {
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
if err := gui.switchContext(gui.Contexts.Staging.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Staging.Context); err != nil {
return err
}
@@ -118,7 +116,7 @@ func (gui *Gui) handleResetSelection() error {
})
},
handleClose: func() error {
return gui.switchContext(gui.Contexts.Staging.Context)
return gui.pushContext(gui.Contexts.Staging.Context)
},
})
} else {
+9 -5
View File
@@ -117,11 +117,15 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.Tr.NoTrackedStagedFilesStash)
}
return gui.prompt(gui.Tr.StashChanges, "", func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
return gui.prompt(promptOpts{
title: gui.Tr.StashChanges,
handleConfirm: func(stashComment string) error {
if err := stashFunc(stashComment); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
},
})
}
+8 -7
View File
@@ -6,6 +6,7 @@ import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -33,7 +34,7 @@ func (gui *Gui) refreshStatus() {
status = utils.ColoredString(fmt.Sprintf("↑%s↓%s ", currentBranch.Pushables, currentBranch.Pullables), trackColor)
}
if gui.GitCommand.WorkingTreeState() != "normal" {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
status += utils.ColoredString(fmt.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()), color.FgYellow)
}
@@ -57,7 +58,7 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(v, gui.Tr.CheckingForUpdates)
return gui.createLoaderPanel(gui.Tr.CheckingForUpdates)
}
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
@@ -72,7 +73,7 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
return nil
}
if err := gui.switchContext(gui.Contexts.Status.Context); err != nil {
if err := gui.pushContext(gui.Contexts.Status.Context); err != nil {
return err
}
@@ -80,7 +81,7 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
repoName := utils.GetCurrentRepoName()
switch gui.GitCommand.WorkingTreeState() {
case "rebasing", "merging":
case commands.REBASE_MODE_REBASING, commands.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return gui.handleCreateRebaseOptionsMenu()
@@ -149,11 +150,11 @@ func lazygitTitle() string {
func (gui *Gui) workingTreeState() string {
rebaseMode, _ := gui.GitCommand.RebaseMode()
if rebaseMode != "" {
return "rebasing"
return commands.REBASE_MODE_REBASING
}
merging, _ := gui.GitCommand.IsInMergeState()
if merging {
return "merging"
return commands.REBASE_MODE_MERGING
}
return "normal"
return commands.REBASE_MODE_NORMAL
}
+1 -1
View File
@@ -97,7 +97,7 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
gui.State.Panels.SubCommits.SelectedLineIdx = 0
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
return gui.switchContext(gui.Contexts.SubCommits.Context)
return gui.pushContext(gui.Contexts.SubCommits.Context)
}
func (gui *Gui) handleSwitchToSubCommits() error {
+33 -16
View File
@@ -126,30 +126,47 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
}
func (gui *Gui) handleAddSubmodule() error {
return gui.prompt(gui.Tr.LcNewSubmoduleUrl, "", func(submoduleUrl string) error {
nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl)))
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmoduleUrl,
handleConfirm: func(submoduleUrl string) error {
nameSuggestion := filepath.Base(strings.TrimSuffix(submoduleUrl, filepath.Ext(submoduleUrl)))
return gui.prompt(gui.Tr.LcNewSubmoduleName, nameSuggestion, func(submoduleName string) error {
return gui.prompt(gui.Tr.LcNewSubmodulePath, submoduleName, func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmoduleName,
initialContent: nameSuggestion,
handleConfirm: func(submoduleName string) error {
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
return gui.prompt(promptOpts{
title: gui.Tr.LcNewSubmodulePath,
initialContent: submoduleName,
handleConfirm: func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
},
})
},
})
})
},
})
}
func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error {
return gui.prompt(fmt.Sprintf(gui.Tr.LcUpdateSubmoduleUrl, submodule.Name), submodule.Url, func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.prompt(promptOpts{
title: fmt.Sprintf(gui.Tr.LcUpdateSubmoduleUrl, submodule.Name),
initialContent: submodule.Url,
handleConfirm: func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
})
},
})
}
+36
View File
@@ -0,0 +1,36 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
func (gui *Gui) getSelectedSuggestionValue() string {
selectedSuggestion := gui.getSelectedSuggestion()
if selectedSuggestion != nil {
return selectedSuggestion.Value
}
return ""
}
func (gui *Gui) getSelectedSuggestion() *types.Suggestion {
selectedLine := gui.State.Panels.Suggestions.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.Suggestions[selectedLine]
}
func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) {
view := gui.getSuggestionsView()
if view == nil {
return
}
gui.State.Suggestions = suggestions
gui.State.Panels.Suggestions.SelectedLineIdx = 0
_ = gui.resetOrigin(view)
_ = gui.Contexts.Suggestions.Context.HandleRender()
}
+31 -24
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
}
}
},
})
},
})
})
}
+8
View File
@@ -0,0 +1,8 @@
package types
type Suggestion struct {
// value is the thing that we're matching on and the thing that will be submitted if you select the suggestion
Value string
// label is what is actually displayed so it can e.g. contain color
Label string
}
+3 -2
View File
@@ -2,6 +2,7 @@ package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -87,7 +88,7 @@ func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.UndoingStatus
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing)
}
@@ -118,7 +119,7 @@ func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.RedoingStatus
if gui.GitCommand.WorkingTreeState() == "rebasing" {
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing)
}
+10 -6
View File
@@ -1,11 +1,15 @@
package gui
import "github.com/jesseduffield/gocui"
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) showUpdatePrompt(newVersion string) error {
return gui.ask(askOpts{
title: "New version available!",
prompt: "Download latest version? (enter/esc)",
prompt: fmt.Sprintf("Download version %s? (enter/esc)", newVersion),
handleConfirm: func() error {
gui.startUpdating(newVersion)
return nil
@@ -41,13 +45,13 @@ func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) erro
func (gui *Gui) startUpdating(newVersion string) {
gui.State.Updating = true
gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, gui.onUpdateFinish)
statusId := gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, func(err error) error { return gui.onUpdateFinish(statusId, err) })
}
func (gui *Gui) onUpdateFinish(err error) error {
func (gui *Gui) onUpdateFinish(statusId int, err error) error {
gui.State.Updating = false
gui.statusManager.removeStatus("updating")
gui.statusManager.removeStatus(statusId)
gui.renderString("appStatus", "")
if err != nil {
return gui.createErrorPanel("Update failed: " + err.Error())
+26 -24
View File
@@ -110,9 +110,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshCommits() })
go utils.Safe(func() { _ = gui.refreshCommits() })
} else {
gui.refreshCommits()
_ = gui.refreshCommits()
}
wg.Done()
}()
@@ -122,9 +122,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshFilesAndSubmodules() })
go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() })
} else {
gui.refreshFilesAndSubmodules()
_ = gui.refreshFilesAndSubmodules()
}
wg.Done()
}()
@@ -134,9 +134,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshStashEntries() })
go utils.Safe(func() { _ = gui.refreshStashEntries() })
} else {
gui.refreshStashEntries()
_ = gui.refreshStashEntries()
}
wg.Done()
}()
@@ -146,9 +146,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshTags() })
go utils.Safe(func() { _ = gui.refreshTags() })
} else {
gui.refreshTags()
_ = gui.refreshTags()
}
wg.Done()
}()
@@ -158,9 +158,9 @@ func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg.Add(1)
func() {
if options.mode == ASYNC {
go utils.Safe(func() { gui.refreshRemotes() })
go utils.Safe(func() { _ = gui.refreshRemotes() })
} else {
gui.refreshRemotes()
_ = gui.refreshRemotes()
}
wg.Done()
}()
@@ -244,11 +244,6 @@ func (gui *Gui) getFilesView() *gocui.View {
return v
}
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
@@ -264,20 +259,27 @@ func (gui *Gui) getMainView() *gocui.View {
return v
}
func (gui *Gui) getSuggestionsView() *gocui.View {
v, _ := gui.g.View("suggestions")
return v
}
func (gui *Gui) getSecondaryView() *gocui.View {
v, _ := gui.g.View("secondary")
return v
}
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
// currently unused
// func (gui *Gui) getStashView() *gocui.View {
// v, _ := gui.g.View("stash")
// return v
// }
func (gui *Gui) getCommitFilesView() *gocui.View {
v, _ := gui.g.View("commitFiles")
return v
}
// currently unused
// func (gui *Gui) getCommitFilesView() *gocui.View {
// v, _ := gui.g.View("commitFiles")
// return v
// }
func (gui *Gui) getMenuView() *gocui.View {
v, _ := gui.g.View("menu")
@@ -418,7 +420,7 @@ func (gui *Gui) clearEditorView(v *gocui.View) {
func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error {
context := gui.ViewTabContextMap[viewName][tabIndex].contexts[0]
return gui.switchContext(context)
return gui.pushContext(context)
}
func (gui *Gui) handleNextTab(g *gocui.Gui, v *gocui.View) error {
+5
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",
}
}
+39 -1
View File
@@ -164,8 +164,10 @@ type TranslationSet struct {
LcMergeIntoCurrentBranch string
ConfirmQuit string
SwitchRepo string
LcAllBranchesLogGraph string
UnsupportedGitService string
LcCreatePullRequest string
LcCopyPullRequestURL string
NoBranchOnRemote string
LcFetch string
NoAutomaticGitFetchTitle string
@@ -428,10 +430,40 @@ type TranslationSet struct {
SubCommitsTitle string
SubmodulesTitle string
NavigationTitle string
SuggestionsTitle 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 +694,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`,
@@ -927,6 +961,10 @@ func englishTranslationSet() TranslationSet {
SubCommitsTitle: "Sub-commits",
SubmodulesTitle: "Submodules",
NavigationTitle: "List Panel Navigation",
SuggestionsTitle: "Suggestions",
PushingTagStatus: "pushing tag",
PullRequestURLCopiedToClipboard: "Pull request URL copied to clipboard",
CommitMessageCopiedToClipboard: "Commit message copied to clipboard",
LcCopiedToClipboard: "copied to clipboard",
}
}
-8
View File
@@ -2,19 +2,11 @@ package i18n
import (
"fmt"
"io/ioutil"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func getDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// TestDetectLanguage is a function.
func TestDetectLanguage(t *testing.T) {
type scenario struct {
+5
View File
@@ -125,8 +125,10 @@ func polishTranslationSet() TranslationSet {
LcRefreshFiles: `odśwież pliki`,
LcMergeIntoCurrentBranch: `scal do obecnej gałęzi`,
ConfirmQuit: `Na pewno chcesz wyjść z programu?`,
LcAllBranchesLogGraph: `pokazywać wszystkie logi branżowe`,
UnsupportedGitService: `Nieobsługiwana usługa git`,
LcCreatePullRequest: `utwórz żądanie wyciągnięcia`,
LcCopyPullRequestURL: `skopiuj adres URL żądania ściągnięcia do schowka`,
NoBranchOnRemote: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
LcFetch: `fetch`,
NoAutomaticGitFetchTitle: `No automatic git fetch`,
@@ -250,5 +252,8 @@ func polishTranslationSet() TranslationSet {
NoFilesStagedPrompt: "You have not staged any files. Commit all files?",
BranchNotFoundTitle: "Branch not found",
BranchNotFoundPrompt: "Branch not found. Create a new branch named",
PullRequestURLCopiedToClipboard: "URL żądania ściągnięcia skopiowany do schowka",
CommitMessageCopiedToClipboard: "Commit message skopiowany do schowka",
LcCopiedToClipboard: "skopiowany do schowka",
}
}
+11
View File
@@ -0,0 +1,11 @@
// +build !windows
package secureexec
import (
"os/exec"
)
func Command(name string, args ...string) *exec.Cmd {
return exec.Command(name, args...)
}
+30
View File
@@ -0,0 +1,30 @@
// +build windows
package secureexec
import (
"os/exec"
"github.com/cli/safeexec"
)
// calling exec.Command directly on a windows machine poses a security risk due to
// the current directory being searched first before any directories in the PATH
// variable, meaning you might clone a repo that contains a program called 'git'
// which does something malicious when executed.
// see https://github.com/golang/go/issues/38736 for more context. We'll likely
// be able to just throw out this code and switch to the official solution when it exists.
// I consider this a minor security concern because you're just as vulnerable if
// you call `git status` from the command line directly but no harm in playing it
// safe.
func Command(name string, args ...string) *exec.Cmd {
bin, err := safeexec.LookPath(name)
if err != nil {
bin = name
}
return exec.Command(bin, args...)
}
+2 -2
View File
@@ -2,11 +2,11 @@ package test
import (
"os"
"os/exec"
"path/filepath"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -24,7 +24,7 @@ func GenerateRepo(filename string) error {
if err := os.Chdir(testPath); err != nil {
return err
}
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
if output, err := secureexec.Command("bash", filename).CombinedOutput(); err != nil {
return errors.New(string(output))
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
"github.com/stretchr/testify/assert"
)
@@ -27,7 +28,7 @@ func (i *CommandSwapper) SwapCommand(t *testing.T, cmd string, args []string) *e
}
splitCmd = str.ToArgv(i.Replace)
return exec.Command(splitCmd[0], splitCmd[1:]...)
return secureexec.Command(splitCmd[0], splitCmd[1:]...)
}
// CreateMockCommand creates a command function that will verify its receiving the right sequence of commands from lazygit
+42 -12
View File
@@ -92,9 +92,18 @@ func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
}
func (u *Updater) currentVersion() string {
versionNumber := u.Config.GetVersion()
if versionNumber == "unversioned" {
return versionNumber
}
return fmt.Sprintf("v%s", u.Config.GetVersion())
}
func (u *Updater) checkForNewUpdate() (string, error) {
u.Log.Info("Checking for an updated version")
currentVersion := u.Config.GetVersion()
currentVersion := u.currentVersion()
if err := u.RecordLastUpdateCheck(); err != nil {
return "", err
}
@@ -214,12 +223,16 @@ func (u *Updater) mappedArch(arch string) string {
return arch
}
func (u *Updater) zipExtension() string {
if runtime.GOOS == "windows" {
return "zip"
}
return "tar.gz"
}
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
extension := "tar.gz"
if runtime.GOOS == "windows" {
extension = "zip"
}
url := fmt.Sprintf(
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
PROJECT_URL,
@@ -227,7 +240,7 @@ func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
newVersion[1:],
u.mappedOs(runtime.GOOS),
u.mappedArch(runtime.GOARCH),
extension,
u.zipExtension(),
)
u.Log.Info("Url for latest release is " + url)
return url, nil
@@ -256,11 +269,16 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
configDir := u.Config.GetUserConfigDir()
u.Log.Info("Download directory is " + configDir)
tempPath := filepath.Join(configDir, "temp_lazygit")
u.Log.Info("Temp path to binary is " + tempPath)
zipPath := filepath.Join(configDir, "temp_lazygit."+u.zipExtension())
u.Log.Info("Temp path to tarball/zip file is " + zipPath)
// Create the file
out, err := os.Create(tempPath)
// remove existing zip file
if err := os.RemoveAll(zipPath); err != nil && !os.IsNotExist(err) {
return err
}
// Create the zip file
out, err := os.Create(zipPath)
if err != nil {
return err
}
@@ -284,6 +302,18 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
return err
}
u.Log.Info("untarring tarball/unzipping zip file")
if err := u.OSCommand.RunCommand("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit"); err != nil {
return err
}
// the `tar` terminal cannot store things in a new location without permission
// so it creates it in the current directory. As such our path is fairly simple.
// You won't see it because it's gitignored.
tempLazygitFilePath := "lazygit"
u.Log.Infof("Path to temp binary is %s", tempLazygitFilePath)
// get the path of the current binary
binaryPath, err := osext.Executable()
if err != nil {
@@ -292,12 +322,12 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
u.Log.Info("Binary path is " + binaryPath)
// Verify the main file exists
if _, err := os.Stat(tempPath); err != nil {
if _, err := os.Stat(zipPath); err != nil {
return err
}
// swap out the old binary for the new one
err = os.Rename(tempPath, binaryPath)
err = os.Rename(tempLazygitFilePath, binaryPath)
if err != nil {
return err
}
+23
View File
@@ -0,0 +1,23 @@
package utils
import (
"sort"
"github.com/sahilm/fuzzy"
)
func FuzzySearch(needle string, haystack []string) []string {
if needle == "" {
return []string{}
}
matches := fuzzy.Find(needle, haystack)
sort.Sort(matches)
result := make([]string, len(matches))
for i, match := range matches {
result[i] = match.Str
}
return result
}
+53
View File
@@ -0,0 +1,53 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestFuzzySearch is a function.
func TestFuzzySearch(t *testing.T) {
type scenario struct {
needle string
haystack []string
expected []string
}
scenarios := []scenario{
{
needle: "",
haystack: []string{"test"},
expected: []string{},
},
{
needle: "test",
haystack: []string{"test"},
expected: []string{"test"},
},
{
needle: "o",
haystack: []string{"a", "o", "e"},
expected: []string{"o"},
},
{
needle: "mybranch",
haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
expected: []string{"mybranch", "my_branch", "this is my branch"},
},
{
needle: "test",
haystack: []string{"not a good match", "this 'test' is a good match", "test"},
expected: []string{"test", "this 'test' is a good match"},
},
{
needle: "test",
haystack: []string{"Test"},
expected: []string{"Test"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
}
}
+1
View File
@@ -81,6 +81,7 @@ func localisedTitle(mApp *app.App, str string) string {
"search": tr.SearchTitle,
"secondary": tr.SecondaryTitle,
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsTitle,
}
title, ok := contextTitleMap[str]
+3 -2
View File
@@ -11,9 +11,10 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
func main() {
@@ -49,7 +50,7 @@ func main() {
func runCommand(args ...string) {
fmt.Println(strings.Join(args, " "))
cmd := exec.Command(args[0], args[1:]...)
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
@@ -0,0 +1 @@
initial commit
@@ -0,0 +1 @@
ref: refs/heads/three
@@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI

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