mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8288de0c84 | ||
|
|
1da2afd450 | ||
|
|
03de51747e | ||
|
|
3d698cd7c1 | ||
|
|
a48cc245e7 | ||
|
|
9ed3a8ee05 | ||
|
|
64daf1310d | ||
|
|
e5ba0d9d9c | ||
|
|
50e4e9d58d | ||
|
|
03b9db5e0a | ||
|
|
043cb2ea44 | ||
|
|
a62d70fbd5 | ||
|
|
053e80a08e | ||
|
|
b726dcc770 | ||
|
|
9df133ed8c | ||
|
|
50dd7b00c3 | ||
|
|
ccbd2c924b | ||
|
|
52d5c3beeb | ||
|
|
c43416891e | ||
|
|
56a573de86 | ||
|
|
e7fff2529c | ||
|
|
78867647d1 | ||
|
|
09f32d4f84 | ||
|
|
6f0f70bd92 | ||
|
|
6df15ddf6e | ||
|
|
922c0887f1 | ||
|
|
d7c9243880 | ||
|
|
f42a595aba | ||
|
|
797722ec12 | ||
|
|
bb4bf23c5c | ||
|
|
f3aacbd253 | ||
|
|
106fce26b5 | ||
|
|
caf208b0a4 | ||
|
|
13b9a8bc9a | ||
|
|
14ce230683 | ||
|
|
f31fbc10f6 | ||
|
|
be404068ff | ||
|
|
5671ec5f58 | ||
|
|
da3b0bf7c8 | ||
|
|
90ade3225f | ||
|
|
4928d1d490 | ||
|
|
9c52eb9d6f | ||
|
|
0a58cb2877 | ||
|
|
7581830e70 | ||
|
|
d468866746 | ||
|
|
999e170f1d | ||
|
|
7513bfb13a | ||
|
|
1f27002b84 | ||
|
|
669bfe763a | ||
|
|
860370a845 | ||
|
|
196761a40a | ||
|
|
26d5444919 | ||
|
|
e05c41828c | ||
|
|
c4cce58464 | ||
|
|
f7e6d4e724 | ||
|
|
d02e992265 | ||
|
|
3e13936e08 | ||
|
|
a3dfcd5a95 | ||
|
|
ce928dc6c8 | ||
|
|
1dea988cd6 | ||
|
|
74bb6f0012 | ||
|
|
79888d3bde | ||
|
|
4e1d3e45a3 | ||
|
|
682db77401 | ||
|
|
6faed08d9d | ||
|
|
62b200a4be | ||
|
|
f7bab5fdc0 | ||
|
|
5ff0ac2816 |
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+28
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
* Linux: `~/.config/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
## 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'
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -3,7 +3,6 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -25,7 +24,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
|
||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
+31
-4
@@ -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
|
||||
@@ -20,7 +22,7 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
return c.OSCommand.RunCommand("git add -- %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
@@ -35,9 +37,9 @@ func (c *GitCommand) UnstageAll() error {
|
||||
|
||||
// UnStageFile unstages a file
|
||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
command := "git rm --cached --force %s"
|
||||
command := "git rm --cached --force -- %s"
|
||||
if tracked {
|
||||
command = "git reset HEAD %s"
|
||||
command = "git reset HEAD -- %s"
|
||||
}
|
||||
|
||||
// renamed files look like "file1 -> file2"
|
||||
@@ -147,7 +149,7 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cache
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges && !cached {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
@@ -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
@@ -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)
|
||||
|
||||
+275
-115
@@ -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")
|
||||
@@ -1040,9 +1037,9 @@ func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"add", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StageFile("test.txt"))
|
||||
@@ -1062,9 +1059,9 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
"Remove an untracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"rm", "--cached", "--force", "test.txt"}, args)
|
||||
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)
|
||||
@@ -1075,9 +1072,9 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
"Remove a tracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "HEAD", "test.txt"}, args)
|
||||
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",
|
||||
@@ -1446,9 +1455,9 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"File not tracked and file has no staged changes",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -14,7 +14,7 @@ func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchM
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
|
||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -26,13 +26,12 @@ func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchM
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
changeStatus := line[0:1]
|
||||
name := line[2:]
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
status := patch.UNSELECTED
|
||||
if patchManager != nil && patchManager.To == parent {
|
||||
status = patchManager.GetFileStatus(name)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -37,7 +37,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
filename := statusString[3:]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
@@ -72,7 +72,13 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
statusLines, err := c.OSCommand.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
statusLines = strings.Replace(statusLines, "\x00", "\n", -1)
|
||||
return statusLines, nil
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,45 +15,42 @@ 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
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
FallbackEscapedQuote string
|
||||
OS string
|
||||
CatCmd string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +127,7 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = strconv.Quote(commandStr)
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
@@ -235,31 +231,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 {
|
||||
@@ -272,20 +243,19 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
if strings.Contains(message, c.Platform.EscapedQuote) {
|
||||
escapedQuote = c.Platform.FallbackEscapedQuote
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
@@ -471,12 +441,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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ import (
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: "cat",
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: "'",
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
FallbackEscapedQuote: "\"",
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: "cat",
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,162 +110,12 @@ 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()
|
||||
|
||||
osCommand.Platform.OS = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
@@ -280,7 +131,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.FallbackEscapedQuote + "hello 'test'" + osCommand.Platform.FallbackEscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -293,18 +144,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \"test\"" + osCommand.Platform.EscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandUnquote is a function.
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
expected := "hello test"
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: "type",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
FallbackEscapedQuote: "\\'",
|
||||
OS: "windows",
|
||||
CatCmd: "cmd /c type",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ type AppConfigurer interface {
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
@@ -82,20 +83,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
|
||||
@@ -198,6 +204,16 @@ func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.UserConfig = userConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func configFilePath(filename string) (string, error) {
|
||||
folder, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
|
||||
+35
-16
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.Replace(input, " ", "-", -1)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+23
-3
@@ -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,
|
||||
@@ -449,6 +464,9 @@ func (gui *Gui) Run() error {
|
||||
}
|
||||
|
||||
g.OnSearchEscape = gui.onSearchEscape
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
return nil
|
||||
}
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
|
||||
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
|
||||
@@ -480,7 +498,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 +600,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 +646,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 +657,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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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}})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
+7
-2
@@ -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`,
|
||||
@@ -301,8 +303,8 @@ func dutchTranslationSet() TranslationSet {
|
||||
LcAddNewRemote: `voeg een nieuwe remote toe`,
|
||||
LcNewRemoteName: `Nieuwe remote name:`,
|
||||
LcNewRemoteUrl: `Nieuwe remote url:`,
|
||||
LcEditRemoteName: `Enter updated remote naam voor {{ .remoteName }}:`,
|
||||
LcEditRemoteUrl: `Enter updated remote url voor {{ .remoteName }}:`,
|
||||
LcEditRemoteName: `Enter updated remote naam voor {{.remoteName}}:`,
|
||||
LcEditRemoteUrl: `Enter updated remote url voor {{.remoteName}}:`,
|
||||
LcRemoveRemote: `verwijder remote`,
|
||||
LcRemoveRemotePrompt: "Weet je zeker dat je deze remote wilt verwijderen",
|
||||
DeleteRemoteBranch: "Verwijder Remote Branch",
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
+68
-3
@@ -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,67 @@ 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.26 Release Notes
|
||||
|
||||
- Config changes applied after editing from within lazygit, no reload required.
|
||||
|
||||
- LOTS of fixes for rendering filenames with strange characters, escaped
|
||||
characters, and UI fixes, by the amazing @Ryooooooga!
|
||||
|
||||
- Also thanks to @Isti115
|
||||
|
||||
## lazygit 0.25 Release Notes
|
||||
|
||||
- Fixes for windows, thanks @murphy66!
|
||||
|
||||
- Allow mapping spaces to dashes when creating a branch, thanks @caquillo07!
|
||||
|
||||
- Allow configuring file refresh and fetch frequency, thanks @Liberatys!
|
||||
|
||||
- Minor security improvement
|
||||
|
||||
- Wide characters supported when entering commit messages, thanks @Ryooooooga!
|
||||
|
||||
- Original branch name appears when renaming, thanks piresrui!
|
||||
|
||||
- Better menus, thanks @1jz!
|
||||
|
||||
- Also thanks to @snipem, @dbast, and @dawidd6
|
||||
|
||||
## 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 +721,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`,
|
||||
@@ -810,8 +871,8 @@ func englishTranslationSet() TranslationSet {
|
||||
LcAddNewRemote: `add new remote`,
|
||||
LcNewRemoteName: `New remote name:`,
|
||||
LcNewRemoteUrl: `New remote url:`,
|
||||
LcEditRemoteName: `Enter updated remote name for {{ .remoteName }}:`,
|
||||
LcEditRemoteUrl: `Enter updated remote url for {{ .remoteName }}:`,
|
||||
LcEditRemoteName: `Enter updated remote name for {{.remoteName}}:`,
|
||||
LcEditRemoteUrl: `Enter updated remote url for {{.remoteName}}:`,
|
||||
LcRemoveRemote: `remove remote`,
|
||||
LcRemoveRemotePrompt: "Are you sure you want to remove remote",
|
||||
DeleteRemoteBranch: "Delete Remote Branch",
|
||||
@@ -927,6 +988,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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// +build !windows
|
||||
|
||||
package secureexec
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Command(name string, args ...string) *exec.Cmd {
|
||||
return exec.Command(name, args...)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user