Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f45db8f7c | ||
|
|
3fb478a30e | ||
|
|
5d12a6bf99 | ||
|
|
1d40d03bb2 | ||
|
|
9a9e3d506d | ||
|
|
06ca71e955 | ||
|
|
308a3b51b3 | ||
|
|
ccd80a0e4b | ||
|
|
37be9dbea1 | ||
|
|
f6ec7babf5 | ||
|
|
802cfb1a04 | ||
|
|
2fc1498517 | ||
|
|
7a464ae5b7 | ||
|
|
927ee63106 | ||
|
|
9989c96321 | ||
|
|
f91892b8f1 | ||
|
|
72bce201df | ||
|
|
5df0fe0765 | ||
|
|
c47c539e12 | ||
|
|
c96496c3a7 | ||
|
|
7561703e8d | ||
|
|
b04b457246 | ||
|
|
6457800748 | ||
|
|
7d9461877a | ||
|
|
e122f421e6 | ||
|
|
6171690b00 | ||
|
|
253504a094 | ||
|
|
f704707d29 | ||
|
|
01d82749b1 | ||
|
|
3eb124c732 | ||
|
|
ef544e6ce9 | ||
|
|
629494144f | ||
|
|
b6a5e9d615 | ||
|
|
5011cac7ea | ||
|
|
5df0475612 | ||
|
|
f6e316dfe5 | ||
|
|
91e8765d9c | ||
|
|
80a8e9b04d | ||
|
|
2008c39516 | ||
|
|
6388af70ac | ||
|
|
5ee559b896 | ||
|
|
ca7252ef8e | ||
|
|
a496858c62 | ||
|
|
40bc3aa5a9 | ||
|
|
e4888e924e | ||
|
|
71fdc5c038 | ||
|
|
a05f22efa2 | ||
|
|
c0cd9dd835 | ||
|
|
305f211615 | ||
|
|
d672b7342f | ||
|
|
e7c27b6f4a | ||
|
|
345c90ac05 | ||
|
|
7564e506b5 | ||
|
|
1e50764b4d | ||
|
|
9619d3447f | ||
|
|
4171b7613c | ||
|
|
92f03a7872 | ||
|
|
2dc8396deb | ||
|
|
7b615e3186 | ||
|
|
a2108362de | ||
|
|
87e9d9bdc2 | ||
|
|
b6454755ca | ||
|
|
3621084096 | ||
|
|
8c25aaa687 | ||
|
|
d02e52989e | ||
|
|
913a2fd065 | ||
|
|
db736896bc | ||
|
|
154b6b09cb | ||
|
|
292b780bd8 | ||
|
|
c421f396af | ||
|
|
a1ae2aa277 | ||
|
|
e19b4fe369 | ||
|
|
eb7531b206 | ||
|
|
428ce2d0f2 | ||
|
|
f1fbf1e9f5 | ||
|
|
268d4080b3 | ||
|
|
2c72990838 | ||
|
|
046edd8120 | ||
|
|
c4552aad28 | ||
|
|
5c57c973d6 | ||
|
|
c5f7ad5adb | ||
|
|
663c036ca5 | ||
|
|
c8e9d1b4fc | ||
|
|
ab0117c416 | ||
|
|
652c97d239 | ||
|
|
bd67bba751 | ||
|
|
5193353020 | ||
|
|
a5719c530a | ||
|
|
add3e8783e | ||
|
|
5eff56b557 | ||
|
|
60c87b3e70 | ||
|
|
66d0fd2133 | ||
|
|
f44ae68e99 | ||
|
|
57f7051590 | ||
|
|
0543d43f10 | ||
|
|
ab8f2b7cc4 | ||
|
|
16dbb6f76e | ||
|
|
51383f24bf | ||
|
|
151486dcfb | ||
|
|
c1d2aa61f3 | ||
|
|
63072af5bc | ||
|
|
44d08edfb0 | ||
|
|
f08fdb2873 | ||
|
|
df4eb70ba2 | ||
|
|
6ca42ff720 | ||
|
|
a533f8e1a5 | ||
|
|
cf8ded0b79 | ||
|
|
73548fa15f | ||
|
|
a0e7604f61 | ||
|
|
aedeba4fe3 | ||
|
|
7033a4bd58 | ||
|
|
c3d7de1c18 | ||
|
|
711bd5a670 | ||
|
|
6b68f4f25d | ||
|
|
89ee0a1dee | ||
|
|
2dc6f5f079 | ||
|
|
bdea3b7dcf | ||
|
|
51f05ce08b | ||
|
|
487ad196a7 | ||
|
|
44140adb92 | ||
|
|
508af269fb | ||
|
|
0af0e66586 | ||
|
|
821a59f21d | ||
|
|
5ea3dc7579 | ||
|
|
ac609bd37c | ||
|
|
67cc65930a | ||
|
|
4f66093335 |
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: "security"
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -8,7 +8,14 @@ on:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
name: ci - ${{matrix.os}}
|
||||
runs-on: ${{matrix.os}}
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@@ -27,7 +34,7 @@ jobs:
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
./test.sh
|
||||
bash ./test.sh
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -27,8 +27,10 @@ lazygit
|
||||
|
||||
test/git_server/data
|
||||
test/integration/*/actual/
|
||||
test/integration/*/actual_remote/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too space space
|
||||
# these sample hooks waste too much space
|
||||
test/integration/*/expected/.git_keep/hooks/
|
||||
test/integration/*/expected_remote/hooks/
|
||||
!.git_keep/
|
||||
lazygit.exe
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# Contributing
|
||||
|
||||
|
||||
♥ We love pull requests from everyone !
|
||||
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish
|
||||
to make via issue, email, or any other method with the owners of this repository
|
||||
before making a change.
|
||||
before making a change.
|
||||
|
||||
## So all code changes happen through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively
|
||||
welcome your pull requests:
|
||||
|
||||
@@ -21,15 +20,33 @@ welcome your pull requests:
|
||||
7. Issue that pull request!
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||
|
||||
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be
|
||||
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
||||
covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
||||
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rending windows and handling user input. Here's the typical process to follow:
|
||||
|
||||
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
|
||||
3. Raise a PR on the gocui repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
|
||||
|
||||
```sh
|
||||
./bump_gocui.sh
|
||||
```
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
@@ -52,7 +52,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
|
||||
|
||||
### Binary Releases
|
||||
|
||||
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
|
||||
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
|
||||
|
||||
5
bump_gocui.sh
Executable file
5
bump_gocui.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
|
||||
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
|
||||
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
|
||||
|
||||
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
|
||||
@@ -22,6 +22,7 @@ gui:
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
@@ -35,6 +36,10 @@ gui:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
cherryPickedCommitBgColor:
|
||||
- blue
|
||||
cherryPickedCommitFgColor:
|
||||
- cyan
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
@@ -45,6 +50,8 @@ gui:
|
||||
showRandomTip: true
|
||||
showCommandLog: true
|
||||
commandLogSize: 8
|
||||
authorColors: # in case you're not happy with the randomly assigned colour
|
||||
'John Smith': '#ff0000'
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
@@ -54,8 +61,14 @@ git:
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'auto' # one of 'auto' | 'merge' | 'rebase' | 'ff-only', auto reads from git configuration
|
||||
log:
|
||||
# one of date-order, author-date-order, topo-order.
|
||||
# topo-order makes it easier to read the git log graph, but commits may not
|
||||
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
|
||||
order: 'topo-order'
|
||||
# one of always, never, when-maximised
|
||||
# this determines whether the git graph is rendered in the commits panel
|
||||
showGraph: 'when-maximised'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
@@ -65,6 +78,7 @@ git:
|
||||
parseEmoji: false
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
editCommandTemplate: '{{editor}} {{filename}}'
|
||||
openCommand: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
@@ -93,10 +107,13 @@ keybinding:
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
scrollLeft: 'H' # scroll left within list view
|
||||
scrollRight: 'L' # scroll right within list view
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
@@ -184,6 +201,7 @@ keybinding:
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -205,14 +223,14 @@ keybinding:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
openCommand: 'start "" {{filename}}'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
openCommand: 'xdg-open {{filename}} >/dev/null'
|
||||
```
|
||||
|
||||
### OSX
|
||||
@@ -241,6 +259,38 @@ os:
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
You can specify a line number you are currently at when in the line-by-line mode.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim'
|
||||
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'code'
|
||||
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
|
||||
```
|
||||
|
||||
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||
|
||||
### Overriding default config file location
|
||||
|
||||
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
|
||||
|
||||
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
|
||||
|
||||
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
|
||||
|
||||
```sh
|
||||
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
|
||||
or
|
||||
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
@@ -208,11 +208,11 @@
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -208,11 +208,11 @@
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: cofnij
|
||||
</pre>
|
||||
|
||||
|
||||
23
go.mod
23
go.mod
@@ -11,24 +11,24 @@ require (
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.3.11 // indirect
|
||||
github.com/go-errors/errors v1.4.0
|
||||
github.com/go-errors/errors v1.4.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/gookit/color v1.4.2
|
||||
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.20210614081440-74b42ecad52b
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
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/kyokomi/emoji/v2 v2.2.8
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
@@ -36,13 +36,12 @@ require (
|
||||
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.6.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
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-20210611083646-a4fc73990273 // indirect
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
55
go.sum
55
go.sum
@@ -32,14 +32,13 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.3.11 h1:ECO6WqHGbKZ3HrSL7bG/zArMCmLaNr5vcjjMVnLHpzc=
|
||||
github.com/gdamore/tcell/v2 v2.3.11/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
|
||||
github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
||||
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
@@ -49,14 +48,16 @@ github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
@@ -70,8 +71,10 @@ 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.20210614081440-74b42ecad52b h1:Wc2zx6xKLNaHc7/wIO6iYyDSjcFGN1Osd6tQvbgMmgo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210614081440-74b42ecad52b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474 h1:4H/oJcUmwJpqyXzqfn+lsjQ/bjpm/HszzLrVbCjgqj4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -98,15 +101,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@@ -143,6 +144,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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=
|
||||
@@ -167,30 +170,34 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
||||
7
main.go
7
main.go
@@ -61,6 +61,9 @@ func main() {
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfig := ""
|
||||
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma seperated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if repoPath != "" {
|
||||
@@ -72,6 +75,10 @@ func main() {
|
||||
gitDir = filepath.Join(repoPath, ".git")
|
||||
}
|
||||
|
||||
if customConfig != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||
}
|
||||
|
||||
if useConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,6 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -21,6 +12,17 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// App struct
|
||||
@@ -102,7 +104,10 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
||||
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
// if we are being called in 'demon' mode, we can just return here
|
||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||
@@ -122,7 +127,13 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
|
||||
app.GitCommand, err = commands.NewGitCommand(
|
||||
app.Log,
|
||||
app.OSCommand,
|
||||
app.Tr,
|
||||
app.Config,
|
||||
git_config.NewStdCachedGitConfig(app.Log),
|
||||
)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package app
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package app
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.RunCommand("git checkout -b %s %s", name, base)
|
||||
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
||||
command = "git branch -D"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("%s %s", command, branch)
|
||||
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
@@ -61,7 +61,7 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
if options.Force {
|
||||
forceArg = " --force"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
@@ -73,24 +73,24 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))
|
||||
return strings.TrimSpace(output), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
|
||||
templateValues := map[string]string{
|
||||
"branchName": branchName,
|
||||
"branchName": c.OSCommand.Quote(branchName),
|
||||
}
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -124,7 +124,7 @@ type MergeOpts struct {
|
||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
@@ -144,18 +144,18 @@ func (c *GitCommand) IsHeadDetached() bool {
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + ref)
|
||||
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ func TestGitCommandResetToCommit(t *testing.T) {
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
@@ -50,25 +52,24 @@ func TestGitCommandCommitStr(t *testing.T) {
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
flags: "",
|
||||
expected: "git commit -m \"test\"",
|
||||
expected: "git commit -m " + gitCmd.OSCommand.Quote("test"),
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "test",
|
||||
flags: "--no-verify",
|
||||
expected: "git commit --no-verify -m \"test\"",
|
||||
expected: "git commit --no-verify -m " + gitCmd.OSCommand.Quote("test"),
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
flags: "",
|
||||
expected: "git commit -m \"line1\" -m \"line2\"",
|
||||
expected: "git commit -m " + gitCmd.OSCommand.Quote("line1") + " -m " + gitCmd.OSCommand.Quote("line2"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
|
||||
@@ -15,12 +15,8 @@ func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
return strings.Split(trimmedOutput, "\n")[0]
|
||||
output := c.GitConfig.Get("core.pager")
|
||||
return strings.Split(output, "\n")[0]
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPager(width int) string {
|
||||
@@ -42,11 +38,6 @@ func (c *GitCommand) colorArg() string {
|
||||
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
|
||||
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) UsingGpg() bool {
|
||||
@@ -55,8 +46,5 @@ func (c *GitCommand) UsingGpg() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
return c.GitConfig.GetBool("commit.gpgsign")
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
func TestGitCommandUsingGpg(t *testing.T) {
|
||||
type scenario struct {
|
||||
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(gpgEnabled bool) {
|
||||
assert.False(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
func(string) (string, error) {
|
||||
return "True", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
func(string) (string, error) {
|
||||
return "ON", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
func(string) (string, error) {
|
||||
return "YeS", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
func(string) (string, error) {
|
||||
return "1", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.UsingGpg())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -14,11 +18,13 @@ func NewDummyGitCommand() *GitCommand {
|
||||
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
|
||||
Config: newAppConfig,
|
||||
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -14,7 +16,11 @@ import (
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
buf, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||
@@ -117,14 +123,14 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
|
||||
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return c.RunCommand("git rm %s", quotedFileName)
|
||||
return c.RunCommand("git rm -- %s", quotedFileName)
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
@@ -199,7 +205,7 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
ignoreWhitespaceArg := ""
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
@@ -214,11 +220,11 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
|
||||
ignoreWhitespaceArg = "--ignore-all-space"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, path)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
c.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return err
|
||||
@@ -265,7 +271,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -293,7 +299,7 @@ func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.RunCommand("git rm -r --cached %s", name)
|
||||
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
@@ -321,11 +327,11 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
|
||||
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GetConfigValue("core.editor")
|
||||
editor = c.GitConfig.Get("core.editor")
|
||||
}
|
||||
|
||||
if editor == "" {
|
||||
@@ -346,5 +352,12 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
templateValues := map[string]string{
|
||||
"editor": editor,
|
||||
"filename": c.OSCommand.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
@@ -4,37 +4,15 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
func TestGitCommandCatFile(t *testing.T) {
|
||||
var osCmd string
|
||||
switch os := runtime.GOOS; os {
|
||||
case "windows":
|
||||
osCmd = "type"
|
||||
default:
|
||||
osCmd = "cat"
|
||||
}
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo", "-n", "test")
|
||||
}
|
||||
|
||||
o, err := gitCmd.CatFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", o)
|
||||
}
|
||||
|
||||
// TestGitCommandStageFile is a function.
|
||||
func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
@@ -546,24 +524,21 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
testName string
|
||||
gitConfigMockResponses map[string]string
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
[]*models.Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
@@ -574,9 +549,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
map[string]string{"commit.gpgsign": "true"},
|
||||
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
@@ -587,9 +560,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
[]*models.Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
@@ -631,7 +602,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
@@ -740,28 +711,30 @@ func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
|
||||
// TestEditFileCmdStr is a function.
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
type scenario struct {
|
||||
filename string
|
||||
configEditCommand string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(string, error)
|
||||
filename string
|
||||
configEditCommand string
|
||||
configEditCommandTemplate string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
gitConfigMockResponses map[string]string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
@@ -769,6 +742,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
{
|
||||
"test",
|
||||
"nano",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
@@ -776,17 +750,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -794,17 +767,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
map[string]string{"core.editor": "nano"},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -816,9 +788,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -826,6 +796,7 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -837,17 +808,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "emacs \"test\"", cmdStr)
|
||||
assert.Equal(t, "emacs "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
@@ -855,17 +825,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"test\"", cmdStr)
|
||||
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
@@ -873,22 +842,37 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"file/with space\"", cmdStr)
|
||||
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("file/with space"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"open file/at line",
|
||||
"vim",
|
||||
"{{editor}} +{{line}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vim +1 "+gitCmd.OSCommand.Quote("open file/at line"), cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommandTemplate = s.configEditCommandTemplate
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
@@ -32,32 +34,37 @@ type GitCommand struct {
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGitConfigValue func(string) (string, error)
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
GitConfig git_config.IGitConfig
|
||||
|
||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||
PushToCurrent bool
|
||||
|
||||
// this is just a view that we write to when running certain commands.
|
||||
// Coincidentally at the moment it's the same view that OnRunCommand logs to
|
||||
// but that need not always be the case.
|
||||
GetCmdWriter func() io.Writer
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
|
||||
func NewGitCommand(
|
||||
log *logrus.Entry,
|
||||
osCommand *oscommands.OSCommand,
|
||||
tr *i18n.TranslationSet,
|
||||
config config.AppConfigurer,
|
||||
gitConfig git_config.IGitConfig,
|
||||
) (*GitCommand, error) {
|
||||
var repo *gogit.Repository
|
||||
|
||||
// see what our default push behaviour is
|
||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
||||
pushToCurrent := false
|
||||
if err != nil {
|
||||
log.Errorf("error reading git config: %v", err)
|
||||
} else {
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
pushToCurrent := gitConfig.Get("push.default") == "current"
|
||||
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,14 +75,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
}
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
GitConfig: gitConfig,
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
@@ -246,3 +254,7 @@ func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...int
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
|
||||
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
|
||||
59
pkg/commands/git_config/cached_git_config.go
Normal file
59
pkg/commands/git_config/cached_git_config.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type IGitConfig interface {
|
||||
Get(string) string
|
||||
GetBool(string) bool
|
||||
}
|
||||
|
||||
type CachedGitConfig struct {
|
||||
cache map[string]string
|
||||
getKey func(string) (string, error)
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig {
|
||||
return NewCachedGitConfig(getGitConfigValue, log)
|
||||
}
|
||||
|
||||
func NewCachedGitConfig(getKey func(string) (string, error), log *logrus.Entry) *CachedGitConfig {
|
||||
return &CachedGitConfig{
|
||||
cache: make(map[string]string),
|
||||
getKey: getKey,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) Get(key string) string {
|
||||
if value, ok := self.cache[key]; ok {
|
||||
self.log.Debugf("using cache for key " + key)
|
||||
return value
|
||||
}
|
||||
|
||||
value := self.getAux(key)
|
||||
self.cache[key] = value
|
||||
return value
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) getAux(key string) string {
|
||||
value, err := self.getKey(key)
|
||||
if err != nil {
|
||||
self.log.Debugf("Error getting git config value for key: " + key + ". Error: " + err.Error())
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) GetBool(key string) bool {
|
||||
return isTruthy(self.Get(key))
|
||||
}
|
||||
|
||||
func isTruthy(value string) bool {
|
||||
lcValue := strings.ToLower(value)
|
||||
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
|
||||
}
|
||||
116
pkg/commands/git_config/cached_git_config_test.go
Normal file
116
pkg/commands/git_config/cached_git_config_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetBool(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
mockResponses map[string]string
|
||||
expected bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
map[string]string{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Some other random key is set",
|
||||
map[string]string{"blah": "blah"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
map[string]string{"commit.gpgsign": "True"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
map[string]string{"commit.gpgsign": "ON"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
map[string]string{"commit.gpgsign": "YeS"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
map[string]string{"commit.gpgsign": "1"},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
return fake.Get(key), nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.GetBool("commit.gpgsign")
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
mockResponses map[string]string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"not set",
|
||||
map[string]string{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"is set",
|
||||
map[string]string{"commit.gpgsign": "blah"},
|
||||
"blah",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
return fake.Get(key), nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.Get("commit.gpgsign")
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// verifying that the cache is used
|
||||
count := 0
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
count++
|
||||
assert.Equal(t, "commit.gpgsign", key)
|
||||
return "blah", nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.Get("commit.gpgsign")
|
||||
assert.Equal(t, "blah", result)
|
||||
result = real.Get("commit.gpgsign")
|
||||
assert.Equal(t, "blah", result)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
22
pkg/commands/git_config/fake_git_config.go
Normal file
22
pkg/commands/git_config/fake_git_config.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package git_config
|
||||
|
||||
type FakeGitConfig struct {
|
||||
mockResponses map[string]string
|
||||
}
|
||||
|
||||
func NewFakeGitConfig(mockResponses map[string]string) *FakeGitConfig {
|
||||
return &FakeGitConfig{
|
||||
mockResponses: mockResponses,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) Get(key string) string {
|
||||
if self.mockResponses == nil {
|
||||
return ""
|
||||
}
|
||||
return self.mockResponses[key]
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) GetBool(key string) bool {
|
||||
return isTruthy(self.Get(key))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -209,7 +210,8 @@ func TestNewGitCommand(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.setup()
|
||||
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig()))
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,12 @@ type CommitListBuilder struct {
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
|
||||
func NewCommitListBuilder(
|
||||
log *logrus.Entry,
|
||||
gitCommand *GitCommand,
|
||||
osCommand *oscommands.OSCommand,
|
||||
tr *i18n.TranslationSet,
|
||||
) *CommitListBuilder {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
@@ -88,6 +93,8 @@ type GetCommitsOptions struct {
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
// determines if we show the whole git graph i.e. pass the '--all' flag
|
||||
All bool
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
@@ -110,7 +117,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
|
||||
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,7 +156,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
cmd := c.getLogCmd(opts)
|
||||
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
if canExtractCommit(line) {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
@@ -177,6 +184,51 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
commits, err := c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := make([]string, len(commits))
|
||||
for i, commit := range commits {
|
||||
commitShas[i] = commit.Sha
|
||||
}
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmd := c.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf(
|
||||
"git show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
),
|
||||
)
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
@@ -322,7 +374,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
|
||||
}
|
||||
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
|
||||
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
@@ -339,7 +391,7 @@ func ignoringWarnings(commandOutput string) string {
|
||||
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
||||
// all commits above this are deemed unpushed and marked as such.
|
||||
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -359,18 +411,37 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
config := c.GitCommand.Config.GetUserConfig().Git.Log
|
||||
|
||||
orderFlag := "--" + config.Order
|
||||
allFlag := ""
|
||||
if opts.All {
|
||||
allFlag = " --all"
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf(
|
||||
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
|
||||
opts.RefName,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
|
||||
c.OSCommand.Quote(opts.RefName),
|
||||
orderFlag,
|
||||
allFlag,
|
||||
prettyFormat,
|
||||
limitFlag,
|
||||
20,
|
||||
filterFlag,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var prettyFormat = fmt.Sprintf(
|
||||
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
)
|
||||
|
||||
func canExtractCommit(line string) bool {
|
||||
return strings.Split(line, " ")[0] != "gpg:"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
Log: utils.NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type GetStatusFileOptions struct {
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
||||
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
|
||||
@@ -2,8 +2,8 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@@ -13,26 +13,25 @@ import (
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) <= 1 {
|
||||
fields := strings.SplitN(line, " ", 3)
|
||||
if len(fields) <= 2 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
||||
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
}
|
||||
|
||||
33
pkg/commands/oscommands/cmd_obj.go
Normal file
33
pkg/commands/oscommands/cmd_obj.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line. If you want to log the command you'll use .ToString() and
|
||||
// if you want to run it you'll use .GetCmd()
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
ToString() string
|
||||
AddEnvVars(...string) ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmdStr string
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
return self.cmd
|
||||
}
|
||||
|
||||
func (self *CmdObj) ToString() string {
|
||||
return self.cmdStr
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
self.cmd.Env = append(self.cmd.Env, vars...)
|
||||
|
||||
return self
|
||||
}
|
||||
153
pkg/commands/oscommands/exec_live.go
Normal file
153
pkg/commands/oscommands/exec_live.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string {
|
||||
ttyText = ttyText + " " + word
|
||||
|
||||
prompts := map[string]string{
|
||||
`.+'s password:`: "password",
|
||||
`Password\s*for\s*'.+':`: "password",
|
||||
`Username\s*for\s*'.+':`: "username",
|
||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
|
||||
// separate for windows and other OS's
|
||||
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
|
||||
}
|
||||
|
||||
type cmdHandler struct {
|
||||
stdoutPipe io.Reader
|
||||
stdinPipe io.Writer
|
||||
close func() error
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
|
||||
// Output is a function that executes by every word that gets read by bufio
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't write anything to stdin
|
||||
func RunCommandWithOutputLiveAux(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
// handleOutput takes a word from stdout and returns a string to be written to stdin.
|
||||
// See DetectUnamePass above for how this is used to check for a username/password request
|
||||
handleOutput func(string) string,
|
||||
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
|
||||
) error {
|
||||
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||
c.LogCommand(cmdObj.ToString(), true)
|
||||
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = io.MultiWriter(writer, &stderr)
|
||||
|
||||
handler, err := startCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := handler.close(); closeErr != nil {
|
||||
c.Log.Error(closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
tr := io.TeeReader(handler.stdoutPipe, writer)
|
||||
|
||||
go utils.Safe(func() {
|
||||
scanner := bufio.NewScanner(tr)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
output := strings.Trim(text, " ")
|
||||
toInput := handleOutput(output)
|
||||
if toInput != "" {
|
||||
_, _ = handler.stdinPipe.Write([]byte(toInput))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,98 +1,37 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
||||
// Output is a function that executes by every word that gets read by bufio
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
func RunCommandWithOutputLiveWrapper(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
output func(string) string,
|
||||
) error {
|
||||
return RunCommandWithOutputLiveAux(
|
||||
c,
|
||||
cmdObj,
|
||||
writer,
|
||||
output,
|
||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
scanner := bufio.NewScanner(ptmx)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
toOutput := strings.Trim(scanner.Text(), " ")
|
||||
_, _ = ptmx.WriteString(output(toOutput))
|
||||
}
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
ptmx.Close()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return &cmdHandler{
|
||||
stdoutPipe: ptmx,
|
||||
stdinPipe: ptmx,
|
||||
close: ptmx.Close,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
return c.RunCommand(command)
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||
func RunCommandWithOutputLiveWrapper(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
output func(string) string,
|
||||
) error {
|
||||
return RunCommandWithOutputLiveAux(
|
||||
c,
|
||||
cmdObj,
|
||||
writer,
|
||||
output,
|
||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// because we don't yet have windows support for a pty, we instead just
|
||||
// pass our standard stream handlers and because there's no pty to close
|
||||
// we pass a no-op function for that.
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -24,10 +23,8 @@ import (
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
@@ -203,7 +200,14 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
quotedCommand = strings.NewReplacer(
|
||||
"^", "^^",
|
||||
"&", "^&",
|
||||
"|", "^|",
|
||||
"<", "^<",
|
||||
">", "^>",
|
||||
"%", "^%",
|
||||
).Replace(commandStr)
|
||||
} else {
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
@@ -212,50 +216,6 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
|
||||
ttyText = ttyText + " " + word
|
||||
|
||||
prompts := map[string]string{
|
||||
`.+'s password:`: "password",
|
||||
`Password\s*for\s*'.+':`: "password",
|
||||
`Username\s*for\s*'.+':`: "username",
|
||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// RunCommand runs a command and just returns the error
|
||||
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
@@ -265,7 +225,7 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
|
||||
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
|
||||
// need access to the shell
|
||||
func (c *OSCommand) RunShellCommand(command string) error {
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
cmd := c.ShellCommandFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
@@ -301,17 +261,11 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
|
||||
quoted := c.Quote(filename)
|
||||
if c.Platform.OS == "linux" {
|
||||
// Add extra quoting to avoid issues with shell command string
|
||||
quoted = c.Quote(quoted)
|
||||
quoted = quoted[1 : len(quoted)-1]
|
||||
}
|
||||
templateValues := map[string]string{
|
||||
"filename": quoted,
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
err := c.RunShellCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -324,7 +278,7 @@ func (c *OSCommand) OpenLink(link string) error {
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
err := c.RunShellCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -346,17 +300,23 @@ func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
var quote string
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
quote = `\"`
|
||||
message = strings.NewReplacer(
|
||||
`"`, `"'"'"`,
|
||||
`\"`, `\\"`,
|
||||
).Replace(message)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
quote = `"`
|
||||
message = strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`"`, `\"`,
|
||||
`$`, `\$`,
|
||||
"`", "\\`",
|
||||
).Replace(message)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
return quote + message + quote
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
@@ -559,7 +519,9 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
|
||||
escaped := strings.Replace(str, "\n", "\\n", -1)
|
||||
truncated := utils.TruncateWithEllipsis(escaped, 40)
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", truncated), false)
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
@@ -568,3 +530,30 @@ func (c *OSCommand) RemoveFile(path string) error {
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(cmd.Args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
@@ -9,10 +10,8 @@ import (
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: []string{"cat"},
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
|
||||
138
pkg/commands/oscommands/os_default_test.go
Normal file
138
pkg/commands/oscommands/os_default_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileDarwin is a function.
|
||||
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "test"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Platform.OS = "darwin"
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFileLinux tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "linux"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@ package oscommands
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -59,132 +57,6 @@ func TestOSCommandRunCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFile is a function.
|
||||
func TestOSCommandOpenFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Platform.OS = "darwin"
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFile tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"test\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"filename with spaces\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"let's_test_with_single_quote\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"\\$USER.txt\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "linux"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `sh -c "xdg-open {{filename}} > /dev/null"`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
@@ -193,7 +65,7 @@ func TestOSCommandQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
expected := "\"hello \\`test\\`\""
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -206,7 +78,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
expected := `"hello 'test'"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -219,7 +91,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
||||
expected := `"hello \"test\""`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -230,9 +102,9 @@ func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
actual := osCommand.Quote(`hello "test" 'test2'`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
expected := `\"hello "'"'"test"'"'" 'test2'\"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: []string{"cmd", "/c", "type"},
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
OS: "windows",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
}
|
||||
}
|
||||
|
||||
86
pkg/commands/oscommands/os_windows_test.go
Normal file
86
pkg/commands/oscommands/os_windows_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "windows"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,14 @@ func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string,
|
||||
|
||||
// I want to know, given a hunk, what line a given index is on
|
||||
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
||||
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
|
||||
n := idx - hunk.FirstLineIdx - 1
|
||||
if n < 0 {
|
||||
n = 0
|
||||
} else if n >= len(hunk.bodyLines) {
|
||||
n = len(hunk.bodyLines) - 1
|
||||
}
|
||||
|
||||
lines := hunk.bodyLines[0:n]
|
||||
|
||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||
|
||||
|
||||
@@ -194,6 +194,19 @@ func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndic
|
||||
return result
|
||||
}
|
||||
|
||||
// PlainRenderLines returns the non-coloured string of diff part from firstLineIndex to
|
||||
// lastLineIndex
|
||||
func (p *PatchParser) PlainRenderLines(firstLineIndex, lastLineIndex int) string {
|
||||
linesToCopy := p.PatchLines[firstLineIndex : lastLineIndex+1]
|
||||
|
||||
renderedLines := make([]string, len(linesToCopy))
|
||||
for index, line := range linesToCopy {
|
||||
renderedLines[index] = line.Content
|
||||
}
|
||||
|
||||
return strings.Join(renderedLines, "\n")
|
||||
}
|
||||
|
||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||
// note this will actually include the current index if it is stageable
|
||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||
|
||||
256
pkg/commands/pull_request_default_test.go
Normal file
256
pkg/commands/pull_request_default_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
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 secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
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 secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
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 secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
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 secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
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 secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
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 secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
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 secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
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 secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "darwin"
|
||||
gitCommand.OSCommand.Platform.Shell = "bash"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "-c"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -42,245 +39,3 @@ func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
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 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 secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
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 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 secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
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 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 secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
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 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/avatar&dest=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
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 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/remote-events&dest=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
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 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/operations...feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
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 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 secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "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/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
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 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/commit-ui&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "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/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"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.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
256
pkg/commands/pull_request_windows_test.go
Normal file
256
pkg/commands/pull_request_windows_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequestOnWindows is a function.
|
||||
func TestCreatePullRequestOnWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
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 secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
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 secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
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 secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
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 secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
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 secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
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 secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
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 secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
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 secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "windows"
|
||||
gitCommand.OSCommand.Platform.Shell = "cmd"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "/c"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}`
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,41 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.RunCommand("git remote add %s %s", name, url)
|
||||
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
return c.RunCommand("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
return c.RunCommand("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(command)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branchName,
|
||||
c.OSCommand.Quote(branchName),
|
||||
)
|
||||
|
||||
return err == nil
|
||||
@@ -37,5 +44,5 @@ func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
return c.GetConfigValue("remote.origin.url")
|
||||
return c.GitConfig.Get("remote.origin.url")
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
|
||||
// the set-url command is only for later git versions so we're doing it manually here
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), newUrl); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), c.OSCommand.Quote(newUrl)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,43 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// 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 = ""
|
||||
type PushOpts struct {
|
||||
Force bool
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
PromptUserForCredential func(string) string
|
||||
}
|
||||
|
||||
func (c *GitCommand) Push(opts PushOpts) error {
|
||||
cmdStr := "git push"
|
||||
|
||||
if opts.Force {
|
||||
cmdStr += " --force-with-lease"
|
||||
}
|
||||
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
if opts.SetUpstream {
|
||||
cmdStr += " --set-upstream"
|
||||
}
|
||||
|
||||
setUpstreamArg := ""
|
||||
if upstream != "" {
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
if opts.UpstreamRemote != "" {
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote)
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
if opts.UpstreamBranch != "" {
|
||||
if opts.UpstreamRemote == "" {
|
||||
return errors.New(c.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
@@ -34,16 +49,17 @@ type FetchOptions struct {
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
command := "git fetch"
|
||||
cmdStr := "git fetch"
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
@@ -51,41 +67,45 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
})
|
||||
}
|
||||
|
||||
type PullOptions struct {
|
||||
PromptUserForCredential func(string) string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
FastForwardOnly bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) Pull(opts PullOptions) error {
|
||||
if opts.PromptUserForCredential == nil {
|
||||
return errors.New("PromptUserForCredential is required")
|
||||
}
|
||||
|
||||
cmdStr := "git pull --no-edit"
|
||||
|
||||
if opts.FastForwardOnly {
|
||||
cmdStr += " --ff-only"
|
||||
}
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||
// has 'pull.rebase = interactive' configured.
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
|
||||
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s", remoteName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPullMode(mode string) string {
|
||||
if mode != "auto" {
|
||||
return mode
|
||||
}
|
||||
|
||||
var isRebase bool
|
||||
var isFf bool
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
isRebase = c.GetConfigValue("pull.rebase") == "true"
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
isFf = c.GetConfigValue("pull.ff") == "only"
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if isRebase {
|
||||
return "rebase"
|
||||
} else if isFf {
|
||||
return "ff-only"
|
||||
} else {
|
||||
return "merge"
|
||||
}
|
||||
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
@@ -11,213 +11,154 @@ import (
|
||||
// TestGitCommandPush is a function.
|
||||
func TestGitCommandPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
command func(string, ...string) *exec.Cmd
|
||||
forcePush bool
|
||||
test func(error)
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
opts PushOpts
|
||||
test func(error)
|
||||
}
|
||||
|
||||
prompt := func(passOrUname string) string {
|
||||
return "\n"
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"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 secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"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 secureexec.Command("echo")
|
||||
},
|
||||
true,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
"Push with force disabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
"Push with force enabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{Force: true, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
false,
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with remote branch but no origin",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return nil
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
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"
|
||||
})
|
||||
err := gitCmd.Push(s.opts)
|
||||
s.test(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type getPullModeScenario struct {
|
||||
testName string
|
||||
getGitConfigValueMock func(string) (string, error)
|
||||
configPullModeValue string
|
||||
test func(string)
|
||||
}
|
||||
|
||||
func TestGetPullMode(t *testing.T) {
|
||||
|
||||
scenarios := getPullModeScenarios(t)
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValueMock
|
||||
s.test(gitCmd.GetPullMode(s.configPullModeValue))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getPullModeScenarios(t *testing.T) []getPullModeScenario {
|
||||
return []getPullModeScenario{
|
||||
{
|
||||
testName: "Merge is default",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "merge", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when pull.rebase is true",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads ff-only when pull.ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when rebase is true and ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when pull.rebase is true",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads ff-only when pull.ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects merge config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "merge",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "merge", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects rebase config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "rebase",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects ff-only config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "ff-only",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(tagName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
@@ -12,17 +12,19 @@ import (
|
||||
|
||||
// AppConfig contains the base configuration fields required for lazygit.
|
||||
type AppConfig struct {
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *UserConfig
|
||||
UserConfigDir string
|
||||
UserConfigPath string
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *UserConfig
|
||||
UserConfigPaths []string
|
||||
DeafultConfFiles bool
|
||||
UserConfigDir string
|
||||
TempDir string
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
@@ -35,23 +37,35 @@ type AppConfigurer interface {
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
GetUserConfig() *UserConfig
|
||||
GetUserConfigPaths() []string
|
||||
GetUserConfigDir() string
|
||||
GetUserConfigPath() string
|
||||
GetTempDir() string
|
||||
GetAppState() *AppState
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
ShowCommandLogOnStartup() bool
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
||||
configDir, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
if err != nil && !os.IsPermission(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(configDir)
|
||||
var userConfigPaths []string
|
||||
customConfigFiles := os.Getenv("LG_CONFIG_FILE")
|
||||
if customConfigFiles != "" {
|
||||
// Load user defined config files
|
||||
userConfigPaths = strings.Split(customConfigFiles, ",")
|
||||
} else {
|
||||
// Load default config files
|
||||
userConfigPaths = []string{filepath.Join(configDir, ConfigFilename)}
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(userConfigPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -60,28 +74,35 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||
debuggingFlag = true
|
||||
}
|
||||
|
||||
tempDir := filepath.Join(os.TempDir(), "lazygit")
|
||||
|
||||
appState, err := loadAppState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
UserConfigDir: configDir,
|
||||
UserConfigPath: filepath.Join(configDir, "config.yml"),
|
||||
AppState: appState,
|
||||
IsNewRepo: false,
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
UserConfigPaths: userConfigPaths,
|
||||
UserConfigDir: configDir,
|
||||
TempDir: tempDir,
|
||||
AppState: appState,
|
||||
IsNewRepo: false,
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
func isCustomConfigFile(path string) bool {
|
||||
return path != filepath.Join(ConfigDir(), ConfigFilename)
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
||||
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||
@@ -102,43 +123,45 @@ func configDirForVendor(vendor string) string {
|
||||
|
||||
func findOrCreateConfigDir() (string, error) {
|
||||
folder := ConfigDir()
|
||||
err := os.MkdirAll(folder, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return folder, nil
|
||||
return folder, os.MkdirAll(folder, 0755)
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
|
||||
return loadUserConfig(configDir, GetDefaultConfig())
|
||||
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
|
||||
return loadUserConfig(configFiles, GetDefaultConfig())
|
||||
}
|
||||
|
||||
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
||||
fileName := filepath.Join(configDir, "config.yml")
|
||||
func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error) {
|
||||
for _, path := range configFiles {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fileName); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
file, err := os.Create(fileName)
|
||||
// if use has supplied their own custom config file path(s), we assume
|
||||
// the files have already been created, so we won't go and create them here.
|
||||
if isCustomConfigFile(path) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "read-only file system") {
|
||||
return base, nil
|
||||
if os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(content, base); err != nil {
|
||||
return nil, err
|
||||
if err := yaml.Unmarshal(content, base); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return base, nil
|
||||
@@ -190,22 +213,25 @@ func (c *AppConfig) GetUserConfig() *UserConfig {
|
||||
return c.UserConfig
|
||||
}
|
||||
|
||||
// GetUserConfig returns the user config
|
||||
func (c *AppConfig) GetUserConfigPath() string {
|
||||
return c.UserConfigPath
|
||||
}
|
||||
|
||||
// GetAppState returns the app state
|
||||
func (c *AppConfig) GetAppState() *AppState {
|
||||
return c.AppState
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUserConfigPaths() []string {
|
||||
return c.UserConfigPaths
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetTempDir() string {
|
||||
return c.TempDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,9 +249,11 @@ func configFilePath(filename string) (string, error) {
|
||||
return filepath.Join(folder, filename), nil
|
||||
}
|
||||
|
||||
// ConfigFilename returns the filename of the current config file
|
||||
var ConfigFilename = "config.yml"
|
||||
|
||||
// ConfigFilename returns the filename of the deafult config file
|
||||
func (c *AppConfig) ConfigFilename() string {
|
||||
return filepath.Join(c.UserConfigDir, "config.yml")
|
||||
return filepath.Join(c.UserConfigDir, ConfigFilename)
|
||||
}
|
||||
|
||||
// SaveAppState marshalls the AppState struct and writes it to the disk
|
||||
@@ -240,13 +268,34 @@ func (c *AppConfig) SaveAppState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
if err != nil && os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// originally we could only hide the command log permanently via the config
|
||||
// but now we do it via state. So we need to still support the config for the
|
||||
// sake of backwards compatibility
|
||||
func (c *AppConfig) ShowCommandLogOnStartup() bool {
|
||||
if !c.UserConfig.Gui.ShowCommandLog {
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.AppState.HideCommandLog
|
||||
}
|
||||
|
||||
// loadAppState loads recorded AppState from file
|
||||
func loadAppState() (*AppState, error) {
|
||||
filepath, err := configFilePath("state.yml")
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
return getDefaultAppState(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -274,6 +323,10 @@ type AppState struct {
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
|
||||
// these are for custom commands typed in directly, not for custom commands in the lazygit config
|
||||
CustomCommandsHistory []string
|
||||
HideCommandLog bool
|
||||
}
|
||||
|
||||
func getDefaultAppState() *AppState {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows && !linux
|
||||
// +build !windows,!linux
|
||||
|
||||
package config
|
||||
@@ -5,8 +6,9 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
|
||||
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: `xdg-open {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `cmd /c "start "" {{filename}}"`,
|
||||
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: `start "" {{filename}}`,
|
||||
OpenLinkCommand: `start "" {{link}}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func NewDummyAppConfig() *AppConfig {
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: GetDefaultConfig(),
|
||||
AppState: &AppState{},
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
|
||||
@@ -24,6 +24,7 @@ type RefresherConfig struct {
|
||||
}
|
||||
|
||||
type GuiConfig struct {
|
||||
AuthorColors map[string]string `yaml:"authorColors"`
|
||||
ScrollHeight int `yaml:"scrollHeight"`
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
@@ -32,6 +33,7 @@ type GuiConfig struct {
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||
Language string `yaml:"language"`
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
@@ -43,12 +45,14 @@ type GuiConfig struct {
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
LightTheme bool `yaml:"lightTheme"`
|
||||
ActiveBorderColor []string `yaml:"activeBorderColor"`
|
||||
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
|
||||
OptionsTextColor []string `yaml:"optionsTextColor"`
|
||||
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
|
||||
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
||||
LightTheme bool `yaml:"lightTheme"`
|
||||
ActiveBorderColor []string `yaml:"activeBorderColor"`
|
||||
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
|
||||
OptionsTextColor []string `yaml:"optionsTextColor"`
|
||||
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
|
||||
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
||||
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
|
||||
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
|
||||
}
|
||||
|
||||
type CommitLengthConfig struct {
|
||||
@@ -58,7 +62,6 @@ type CommitLengthConfig struct {
|
||||
type GitConfig struct {
|
||||
Paging PagingConfig `yaml:"paging"`
|
||||
Merging MergingConfig `yaml:"merging"`
|
||||
Pull PullConfig `yaml:"pull"`
|
||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||
@@ -66,7 +69,9 @@ type GitConfig struct {
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
// this shoudl really be under 'gui', not 'git'
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
type PagingConfig struct {
|
||||
@@ -80,8 +85,9 @@ type MergingConfig struct {
|
||||
Args string `yaml:"args"`
|
||||
}
|
||||
|
||||
type PullConfig struct {
|
||||
Mode string `yaml:"mode"`
|
||||
type LogConfig struct {
|
||||
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
|
||||
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
|
||||
}
|
||||
|
||||
type CommitPrefixConfig struct {
|
||||
@@ -108,65 +114,68 @@ type KeybindingConfig struct {
|
||||
|
||||
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||
type KeybindingUniversalConfig struct {
|
||||
Quit string `yaml:"quit"`
|
||||
QuitAlt1 string `yaml:"quit-alt1"`
|
||||
Return string `yaml:"return"`
|
||||
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
|
||||
TogglePanel string `yaml:"togglePanel"`
|
||||
PrevItem string `yaml:"prevItem"`
|
||||
NextItem string `yaml:"nextItem"`
|
||||
PrevItemAlt string `yaml:"prevItem-alt"`
|
||||
NextItemAlt string `yaml:"nextItem-alt"`
|
||||
PrevPage string `yaml:"prevPage"`
|
||||
NextPage string `yaml:"nextPage"`
|
||||
GotoTop string `yaml:"gotoTop"`
|
||||
GotoBottom string `yaml:"gotoBottom"`
|
||||
PrevBlock string `yaml:"prevBlock"`
|
||||
NextBlock string `yaml:"nextBlock"`
|
||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
OptionMenu string `yaml:"optionMenu"`
|
||||
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
|
||||
Select string `yaml:"select"`
|
||||
GoInto string `yaml:"goInto"`
|
||||
Confirm string `yaml:"confirm"`
|
||||
ConfirmAlt1 string `yaml:"confirm-alt1"`
|
||||
Remove string `yaml:"remove"`
|
||||
New string `yaml:"new"`
|
||||
Edit string `yaml:"edit"`
|
||||
OpenFile string `yaml:"openFile"`
|
||||
ScrollUpMain string `yaml:"scrollUpMain"`
|
||||
ScrollDownMain string `yaml:"scrollDownMain"`
|
||||
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
|
||||
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
|
||||
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
|
||||
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
|
||||
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
|
||||
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
|
||||
PushFiles string `yaml:"pushFiles"`
|
||||
PullFiles string `yaml:"pullFiles"`
|
||||
Refresh string `yaml:"refresh"`
|
||||
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
|
||||
NextTab string `yaml:"nextTab"`
|
||||
PrevTab string `yaml:"prevTab"`
|
||||
NextScreenMode string `yaml:"nextScreenMode"`
|
||||
PrevScreenMode string `yaml:"prevScreenMode"`
|
||||
Undo string `yaml:"undo"`
|
||||
Redo string `yaml:"redo"`
|
||||
FilteringMenu string `yaml:"filteringMenu"`
|
||||
DiffingMenu string `yaml:"diffingMenu"`
|
||||
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
Quit string `yaml:"quit"`
|
||||
QuitAlt1 string `yaml:"quit-alt1"`
|
||||
Return string `yaml:"return"`
|
||||
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
|
||||
TogglePanel string `yaml:"togglePanel"`
|
||||
PrevItem string `yaml:"prevItem"`
|
||||
NextItem string `yaml:"nextItem"`
|
||||
PrevItemAlt string `yaml:"prevItem-alt"`
|
||||
NextItemAlt string `yaml:"nextItem-alt"`
|
||||
PrevPage string `yaml:"prevPage"`
|
||||
NextPage string `yaml:"nextPage"`
|
||||
ScrollLeft string `yaml:"scrollLeft"`
|
||||
ScrollRight string `yaml:"scrollRight"`
|
||||
GotoTop string `yaml:"gotoTop"`
|
||||
GotoBottom string `yaml:"gotoBottom"`
|
||||
PrevBlock string `yaml:"prevBlock"`
|
||||
NextBlock string `yaml:"nextBlock"`
|
||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
JumpToBlock []string `yaml:"jumpToBlock"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
OptionMenu string `yaml:"optionMenu"`
|
||||
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
|
||||
Select string `yaml:"select"`
|
||||
GoInto string `yaml:"goInto"`
|
||||
Confirm string `yaml:"confirm"`
|
||||
ConfirmAlt1 string `yaml:"confirm-alt1"`
|
||||
Remove string `yaml:"remove"`
|
||||
New string `yaml:"new"`
|
||||
Edit string `yaml:"edit"`
|
||||
OpenFile string `yaml:"openFile"`
|
||||
ScrollUpMain string `yaml:"scrollUpMain"`
|
||||
ScrollDownMain string `yaml:"scrollDownMain"`
|
||||
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
|
||||
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
|
||||
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
|
||||
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
|
||||
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
|
||||
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
|
||||
PushFiles string `yaml:"pushFiles"`
|
||||
PullFiles string `yaml:"pullFiles"`
|
||||
Refresh string `yaml:"refresh"`
|
||||
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
|
||||
NextTab string `yaml:"nextTab"`
|
||||
PrevTab string `yaml:"prevTab"`
|
||||
NextScreenMode string `yaml:"nextScreenMode"`
|
||||
PrevScreenMode string `yaml:"prevScreenMode"`
|
||||
Undo string `yaml:"undo"`
|
||||
Redo string `yaml:"redo"`
|
||||
FilteringMenu string `yaml:"filteringMenu"`
|
||||
DiffingMenu string `yaml:"diffingMenu"`
|
||||
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -189,6 +198,7 @@ type KeybindingFilesConfig struct {
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
OpenStatusFilter string `yaml:"openStatusFilter"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
@@ -227,6 +237,7 @@ type KeybindingCommitsConfig struct {
|
||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
@@ -255,6 +266,9 @@ type OSConfig struct {
|
||||
// EditCommand is the command for editing a file
|
||||
EditCommand string `yaml:"editCommand,omitempty"`
|
||||
|
||||
// EditCommandTemplate is the command template for editing a file
|
||||
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
|
||||
|
||||
// OpenCommand is the command for opening a file
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
|
||||
@@ -306,19 +320,22 @@ func GetDefaultConfig() *UserConfig {
|
||||
SidePanelWidth: 0.3333,
|
||||
ExpandFocusedSidePanel: false,
|
||||
MainPanelSplitMode: "flexible",
|
||||
Language: "auto",
|
||||
Theme: ThemeConfig{
|
||||
LightTheme: false,
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
InactiveBorderColor: []string{"white"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
LightTheme: false,
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
InactiveBorderColor: []string{"white"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
CherryPickedCommitBgColor: []string{"blue"},
|
||||
CherryPickedCommitFgColor: []string{"cyan"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowFileTree: true,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
@@ -331,8 +348,9 @@ func GetDefaultConfig() *UserConfig {
|
||||
ManualCommit: false,
|
||||
Args: "",
|
||||
},
|
||||
Pull: PullConfig{
|
||||
Mode: "auto",
|
||||
Log: LogConfig{
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
@@ -367,6 +385,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextItemAlt: "j",
|
||||
PrevPage: ",",
|
||||
NextPage: ".",
|
||||
ScrollLeft: "H",
|
||||
ScrollRight: "L",
|
||||
GotoTop: "<",
|
||||
GotoBottom: ">",
|
||||
PrevBlock: "<left>",
|
||||
@@ -375,6 +395,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
JumpToBlock: []string{"1", "2", "3", "4", "5"},
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -435,6 +456,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
OpenMergeTool: "M",
|
||||
OpenStatusFilter: "<c-b>",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
@@ -471,6 +493,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
CheckoutCommit: "<space>",
|
||||
ResetCherryPick: "<c-R>",
|
||||
CopyCommitMessageToClipboard: "<c-y>",
|
||||
OpenLogMenu: "<c-l>",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
|
||||
@@ -210,7 +210,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
|
||||
// The stash window by default only contains one line so that it's not hogging
|
||||
// too much space, but if you access it it should take up some space. This is
|
||||
// the default behaviour when accordian mode is NOT in effect. If it is in effect
|
||||
// the default behaviour when accordion mode is NOT in effect. If it is in effect
|
||||
// then when it's accessed it will have weight 2, not 1.
|
||||
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
|
||||
gui.State.ContextManager.RLock()
|
||||
@@ -259,9 +259,9 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
fullHeightBox("stash"),
|
||||
}
|
||||
} else if height >= 28 {
|
||||
accordianMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordianMode && defaultBox.Window == currentWindow {
|
||||
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: defaultBox.Window,
|
||||
Weight: 2,
|
||||
@@ -276,10 +276,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
Window: "status",
|
||||
Size: 3,
|
||||
},
|
||||
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
||||
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
||||
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
||||
accordianBox(gui.getDefaultStashWindowBox()),
|
||||
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
||||
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
||||
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
||||
accordionBox(gui.getDefaultStashWindowBox()),
|
||||
}
|
||||
} else {
|
||||
squashedHeight := 1
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -220,7 +218,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
func (gui *Gui) handleCheckoutByName() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.BranchName + ":",
|
||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
||||
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
span: "Checkout branch",
|
||||
@@ -298,7 +296,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
if !force && strings.Contains(errMessage, "git branch -D ") {
|
||||
return gui.deleteNamedBranch(selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(errMessage)
|
||||
@@ -414,7 +412,7 @@ func (gui *Gui) handleFastForward() error {
|
||||
_ = gui.createLoaderPanel(message)
|
||||
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
|
||||
_ = gui.pullWithLock(PullFilesOptions{span: span, FastForwardOnly: true})
|
||||
} else {
|
||||
err := gui.GitCommand.WithSpan(span).FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
@@ -535,32 +533,6 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
})
|
||||
}
|
||||
|
||||
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: presentation.GetBranchTextStyle(branchName).Sprint(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 {
|
||||
|
||||
@@ -33,7 +33,7 @@ func (gui *Gui) handleCopyCommit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
item, ok := context.SelectedItem()
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (gui *Gui) getRandomTip() string {
|
||||
constants.Links.Docs.CustomCommands,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
|
||||
"If you ever find a bug, do not hesitate to raise an issue on the repo:\n%s",
|
||||
constants.Links.Issues,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCommitConfirm() error {
|
||||
message := gui.trimmedContent(gui.Views.CommitMessage)
|
||||
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
@@ -22,9 +22,9 @@ func (gui *Gui) handleCommitConfirm() error {
|
||||
|
||||
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||
_ = gui.returnFromContext()
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||
_ = gui.returnFromContext()
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
|
||||
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
|
||||
}
|
||||
|
||||
// RenderCommitLength is a function.
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// after selecting the 200th commit, we'll load in all the rest
|
||||
const COMMIT_THRESHOLD = 200
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
@@ -23,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
|
||||
func (gui *Gui) handleCommitSelect() error {
|
||||
state := gui.State.Panels.Commits
|
||||
if state.SelectedLineIdx > 290 && state.LimitCommits {
|
||||
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
|
||||
state.LimitCommits = false
|
||||
go utils.Safe(func() {
|
||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||
@@ -122,6 +125,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
All: gui.State.ShowWholeGitGraph,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -640,7 +644,7 @@ func (gui *Gui) handleGotoBottomForCommitsPanel() error {
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
if context.GetViewName() == "commits" {
|
||||
return context.handleGotoBottom()
|
||||
}
|
||||
}
|
||||
@@ -667,3 +671,87 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenLogMenu() error {
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowGitGraphAll,
|
||||
onPress: func() error {
|
||||
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
|
||||
|
||||
if gui.State.ShowWholeGitGraph {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.ShowGitGraph,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) {
|
||||
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
|
||||
gui.render()
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "always",
|
||||
onPress: func() error {
|
||||
onSelect("always")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "never",
|
||||
onPress: func() error {
|
||||
onSelect("never")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "when maximised",
|
||||
onPress: func() error {
|
||||
onSelect("when-maximised")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.SortCommits,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) error {
|
||||
gui.Config.GetUserConfig().Git.Log.Order = value
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "topological (topo-order)",
|
||||
onPress: func() error {
|
||||
return onSelect("topo-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("date-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "author-date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("author-date-order")
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// lots of this has been directly ported from one of the example files, will brush up later
|
||||
|
||||
// Copyright 2014 The gocui Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -37,7 +34,6 @@ type askOpts struct {
|
||||
handleConfirm func() error
|
||||
handleClose func() error
|
||||
handlersManageFocus bool
|
||||
findSuggestionsFunc func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
type promptOpts struct {
|
||||
@@ -54,7 +50,6 @@ func (gui *Gui) ask(opts askOpts) error {
|
||||
handleConfirm: opts.handleConfirm,
|
||||
handleClose: opts.handleClose,
|
||||
handlersManageFocus: opts.handlersManageFocus,
|
||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,13 +102,6 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
// we've already closed it so we can just return
|
||||
if !gui.Views.Confirmation.Visible {
|
||||
@@ -169,7 +157,13 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
|
||||
func (gui *Gui) prepareConfirmationPanel(
|
||||
title,
|
||||
prompt string,
|
||||
hasLoader bool,
|
||||
findSuggestionsFunc func(string) []*types.Suggestion,
|
||||
editable bool,
|
||||
) error {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||
// calling SetView on an existing view returns the same view, so I'm not bothering
|
||||
// to reassign to gui.Views.Confirmation
|
||||
@@ -182,20 +176,22 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, f
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
gui.Views.Confirmation.Title = title
|
||||
gui.Views.Confirmation.Wrap = true
|
||||
// for now we do not support wrapping in our editor
|
||||
gui.Views.Confirmation.Wrap = !editable
|
||||
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
||||
|
||||
gui.findSuggestions = findSuggestionsFunc
|
||||
if findSuggestionsFunc != nil {
|
||||
suggestionsViewHeight := 11
|
||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
|
||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suggestionsView.Wrap = true
|
||||
suggestionsView.Wrap = false
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions([]*types.Suggestion{})
|
||||
gui.setSuggestions(findSuggestionsFunc(""))
|
||||
suggestionsView.Visible = true
|
||||
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -209,19 +205,27 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||
// remove any previous keybindings
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
|
||||
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
||||
err := gui.prepareConfirmationPanel(
|
||||
opts.title,
|
||||
opts.prompt,
|
||||
opts.hasLoader,
|
||||
opts.findSuggestionsFunc,
|
||||
opts.editable,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
confirmationView := gui.Views.Confirmation
|
||||
confirmationView.Editable = opts.editable
|
||||
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
|
||||
if opts.editable {
|
||||
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
textArea := confirmationView.TextArea
|
||||
textArea.Clear()
|
||||
textArea.TypeString(opts.prompt)
|
||||
confirmationView.RenderTextArea()
|
||||
} else {
|
||||
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
|
||||
if err := gui.renderStringSync(confirmationView, opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -243,7 +247,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
gui.renderString(gui.Views.Options, actions)
|
||||
var onConfirm func() error
|
||||
if opts.handleConfirmPrompt != nil {
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
|
||||
} else {
|
||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||
}
|
||||
@@ -255,7 +259,11 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
}
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
|
||||
opts.handlersManageFocus,
|
||||
opts.handleConfirmPrompt,
|
||||
gui.getSelectedSuggestionValue,
|
||||
)
|
||||
|
||||
confirmationKeybindings := []confirmationKeybinding{
|
||||
{
|
||||
@@ -276,7 +284,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
|
||||
handler: func() error {
|
||||
if len(gui.State.Suggestions) > 0 {
|
||||
return gui.replaceContext(gui.State.Contexts.Suggestions)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
@@ -309,6 +322,16 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
return f()
|
||||
|
||||
@@ -128,33 +128,37 @@ func (gui *Gui) pushContextWithView(viewName string) error {
|
||||
|
||||
func (gui *Gui) returnFromContext() error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.activateContext(newContext)
|
||||
return gui.returnFromContextSync()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFromContextSync() error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.activateContext(newContext)
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c Context) error {
|
||||
view, _ := gui.g.View(c.GetViewName())
|
||||
|
||||
@@ -281,9 +285,9 @@ func (gui *Gui) currentContextWithoutLock() Context {
|
||||
|
||||
// the status panel is not yet a list context (and may never be), so this method is not
|
||||
// quite the same as currentSideContext()
|
||||
func (gui *Gui) currentSideListContext() *ListContext {
|
||||
func (gui *Gui) currentSideListContext() IListContext {
|
||||
context := gui.currentSideContext()
|
||||
listContext, ok := context.(*ListContext)
|
||||
listContext, ok := context.(IListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -395,6 +399,8 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = oldView.SetOriginX(0)
|
||||
|
||||
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
|
||||
gui.resetWindowForView(gui.Views.CommitFiles)
|
||||
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
|
||||
|
||||
@@ -56,19 +56,19 @@ var allContextKeys = []ContextKey{
|
||||
|
||||
type ContextTree struct {
|
||||
Status Context
|
||||
Files *ListContext
|
||||
Submodules *ListContext
|
||||
Menu *ListContext
|
||||
Branches *ListContext
|
||||
Remotes *ListContext
|
||||
RemoteBranches *ListContext
|
||||
Tags *ListContext
|
||||
BranchCommits *ListContext
|
||||
CommitFiles *ListContext
|
||||
ReflogCommits *ListContext
|
||||
SubCommits *ListContext
|
||||
Stash *ListContext
|
||||
Suggestions *ListContext
|
||||
Files IListContext
|
||||
Submodules IListContext
|
||||
Menu IListContext
|
||||
Branches IListContext
|
||||
Remotes IListContext
|
||||
RemoteBranches IListContext
|
||||
Tags IListContext
|
||||
BranchCommits IListContext
|
||||
CommitFiles IListContext
|
||||
ReflogCommits IListContext
|
||||
SubCommits IListContext
|
||||
Stash IListContext
|
||||
Suggestions IListContext
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
|
||||
@@ -41,9 +41,9 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
|
||||
func (gui *Gui) handleSubmitCredential() error {
|
||||
credentialsView := gui.Views.Credentials
|
||||
message := gui.trimmedContent(credentialsView)
|
||||
message := strings.TrimSpace(credentialsView.TextArea.GetContent())
|
||||
gui.credentials <- message
|
||||
gui.clearEditorView(credentialsView)
|
||||
credentialsView.ClearTextArea()
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
63
pkg/gui/custom_commands_test.go
Normal file
63
pkg/gui/custom_commands_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
cmdOut string
|
||||
filter string
|
||||
valueFormat string
|
||||
labelFormat string
|
||||
test func([]commandMenuEntry, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Extract remote branch name",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
|
||||
"{{ .branch }}",
|
||||
"Remote: {{ .remote }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with empty labelFormat",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .branch }}|{{ .remote }}",
|
||||
"",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with group ids",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .group_2 }}|{{ .group_1 }}",
|
||||
"Remote: {{ .group_1 }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,8 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
|
||||
displayString: gui.Tr.LcEnterRefToDiff,
|
||||
onPress: func() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnteRefName,
|
||||
title: gui.Tr.LcEnteRefName,
|
||||
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
|
||||
handleConfirm: func(response string) error {
|
||||
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
|
||||
// NewDummyGui creates a new dummy GUI for testing
|
||||
func NewDummyUpdater() *updates.Updater {
|
||||
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()))
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
|
||||
return DummyUpdater
|
||||
}
|
||||
|
||||
func NewDummyGui() *Gui {
|
||||
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false)
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
|
||||
return DummyGui
|
||||
}
|
||||
|
||||
@@ -6,90 +6,81 @@ 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) bool {
|
||||
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
textArea.BackSpaceChar()
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
textArea.DeleteChar()
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
textArea.MoveCursorDown()
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
textArea.MoveCursorUp()
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
textArea.MoveCursorLeft()
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
textArea.MoveCursorRight()
|
||||
case key == newlineKey:
|
||||
v.EditNewLine()
|
||||
if allowMultiline {
|
||||
textArea.TypeRune('\n')
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
textArea.TypeRune(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
textArea.ToggleOverwrite()
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
textArea.DeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA || key == gocui.KeyHome:
|
||||
textArea.GoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE || key == gocui.KeyEnd:
|
||||
textArea.GoToEndOfLine()
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
textArea.TypeRune(ch)
|
||||
default:
|
||||
matched = false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 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) bool {
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true)
|
||||
|
||||
// This function is called again on refresh as part of the general resize popup call,
|
||||
// but we need to call it here so that when we go to render the text area it's not
|
||||
// considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
|
||||
err := gui.resizePopupPanel(v, v.TextArea.GetContent())
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
v.RenderTextArea()
|
||||
gui.RenderCommitLength()
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyCtrlD || 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()
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
default:
|
||||
matched = false
|
||||
}
|
||||
v.RenderTextArea()
|
||||
|
||||
if gui.findSuggestions != nil {
|
||||
input := v.Buffer()
|
||||
suggestions := gui.findSuggestions(input)
|
||||
gui.setSuggestions(suggestions)
|
||||
input := v.TextArea.GetContent()
|
||||
gui.suggestionsAsyncHandler.Do(func() func() {
|
||||
suggestions := gui.findSuggestions(input)
|
||||
return func() { gui.setSuggestions(suggestions) }
|
||||
})
|
||||
}
|
||||
|
||||
return matched
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
@@ -11,15 +17,16 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
|
||||
show := !gui.ShowExtrasWindow
|
||||
gui.ShowExtrasWindow = show
|
||||
gui.Config.GetAppState().HideCommandLog = !show
|
||||
_ = gui.Config.SaveAppState()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FocusCommandLog,
|
||||
onPress: func() error {
|
||||
return gui.handleFocusCommandLog()
|
||||
},
|
||||
onPress: gui.handleFocusCommandLog,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,3 +54,28 @@ func (gui *Gui) scrollDownExtra() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getCmdWriter() io.Writer {
|
||||
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
|
||||
}
|
||||
|
||||
// Ensures that the first write is preceded by writing a prefix.
|
||||
// This allows us to say 'Git output:' before writing the actual git output.
|
||||
// We could just write directly to the view in this package before running the command but we already have code in the commands package that writes to the same view beforehand (with the command it's about to run) so things would be out of order.
|
||||
type prefixWriter struct {
|
||||
prefix string
|
||||
prefixWritten bool
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (self *prefixWriter) Write(p []byte) (n int, err error) {
|
||||
if !self.prefixWritten {
|
||||
self.prefixWritten = true
|
||||
// assuming we can write this prefix in one go
|
||||
_, err = self.writer.Write([]byte(self.prefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return self.writer.Write(p)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// macs for some bizarre reason cap the number of watchable files to 256.
|
||||
// there's no obvious platform agonstic way to check the situation of the user's
|
||||
// there's no obvious platform agnostic way to check the situation of the user's
|
||||
// computer so we're just arbitrarily capping at 200. This isn't so bad because
|
||||
// file watching is only really an added bonus for faster refreshing.
|
||||
const MAX_WATCHED_FILES = 50
|
||||
|
||||
@@ -57,13 +57,6 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
// TODO: pull into update task interface
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
@@ -347,9 +340,10 @@ func (gui *Gui) handleWIPCommitPress() error {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
textArea := gui.Views.CommitMessage.TextArea
|
||||
textArea.Clear()
|
||||
textArea.TypeString(skipHookPrefix)
|
||||
gui.Views.CommitMessage.RenderTextArea()
|
||||
|
||||
return gui.handleCommitPress()
|
||||
}
|
||||
@@ -399,10 +393,9 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString(gui.Views.CommitMessage, prefix)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
gui.Views.CommitMessage.TextArea.TypeString(prefix)
|
||||
gui.render()
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -473,14 +466,49 @@ func (gui *Gui) handleCommitEditorPress() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusFilterPressed() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.FilterStagedFiles,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayStaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FilterUnstagedFiles,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayUnstaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.ResetCommitFilterState,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayAll)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) setStatusFiltering(filter filetree.FileManagerDisplayFilter) error {
|
||||
state := gui.State
|
||||
state.FileManager.SetDisplayFilter(filter)
|
||||
return gui.handleRefreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
|
||||
return gui.editFileAtLine(filename, 1)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename, lineNumber)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -630,8 +658,9 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
|
||||
handleConfirm: func(upstream string) error {
|
||||
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
|
||||
errorMessage := err.Error()
|
||||
@@ -649,9 +678,10 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
span string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
FastForwardOnly bool
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
@@ -659,56 +689,53 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mode := &gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
*mode = gui.GitCommand.GetPullMode(*mode)
|
||||
|
||||
// TODO: this doesn't look like a good idea. Why the goroutine?
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(*mode, opts) })
|
||||
go utils.Safe(func() { _ = gui.pullWithLock(opts) })
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
func (gui *Gui) pullWithLock(opts PullFilesOptions) error {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(opts.span)
|
||||
|
||||
err := gitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
err := gitCommand.Pull(
|
||||
commands.PullOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
BranchName: opts.BranchName,
|
||||
FastForwardOnly: opts.FastForwardOnly,
|
||||
},
|
||||
)
|
||||
gui.handleCredentialsPopup(err)
|
||||
if err != nil {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
if err == nil {
|
||||
_ = gui.closeConfirmationPrompt(false)
|
||||
}
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
|
||||
type pushOpts struct {
|
||||
force bool
|
||||
upstreamRemote string
|
||||
upstreamBranch string
|
||||
setUpstream bool
|
||||
}
|
||||
|
||||
func (gui *Gui) push(opts pushOpts) error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
|
||||
return err
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
|
||||
Force: opts.force,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
})
|
||||
|
||||
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
@@ -718,7 +745,10 @@ func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) erro
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(true, upstream, args)
|
||||
newOpts := opts
|
||||
newOpts.force = true
|
||||
|
||||
return gui.push(newOpts)
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -745,27 +775,49 @@ func (gui *Gui) pushFiles() error {
|
||||
if currentBranch.HasCommitsToPull() {
|
||||
return gui.requestToForcePush()
|
||||
} else {
|
||||
return gui.pushWithForceFlag(false, "", "")
|
||||
return gui.push(pushOpts{})
|
||||
}
|
||||
} else {
|
||||
// see if we have an upstream for this branch in our config
|
||||
upstream, err := gui.upstreamForBranchInConfig(currentBranch.Name)
|
||||
upstreamRemote, upstreamBranch, err := gui.upstreamForBranchInConfig(currentBranch.Name)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if upstream != "" {
|
||||
return gui.pushWithForceFlag(false, "", upstream)
|
||||
if upstreamBranch != "" {
|
||||
return gui.push(
|
||||
pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: upstreamRemote,
|
||||
upstreamBranch: upstreamBranch,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(false, "", "--set-upstream")
|
||||
return gui.push(pushOpts{setUpstream: true})
|
||||
} else {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
|
||||
handleConfirm: func(upstream string) error {
|
||||
return gui.pushWithForceFlag(false, upstream, "")
|
||||
var upstreamBranch, upstreamRemote string
|
||||
split := strings.Split(upstream, " ")
|
||||
if len(split) == 2 {
|
||||
upstreamRemote = split[0]
|
||||
upstreamBranch = split[1]
|
||||
} else {
|
||||
upstreamRemote = upstream
|
||||
upstreamBranch = ""
|
||||
}
|
||||
|
||||
return gui.push(pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: upstreamRemote,
|
||||
upstreamBranch: upstreamBranch,
|
||||
setUpstream: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -782,24 +834,24 @@ func (gui *Gui) requestToForcePush() error {
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(true, "", "")
|
||||
return gui.push(pushOpts{force: true})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) upstreamForBranchInConfig(branchName string) (string, error) {
|
||||
func (gui *Gui) upstreamForBranchInConfig(branchName string) (string, string, error) {
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for configBranchName, configBranch := range conf.Branches {
|
||||
if configBranchName == branchName {
|
||||
return fmt.Sprintf("%s %s", configBranch.Remote, configBranchName), nil
|
||||
return configBranch.Remote, configBranchName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge() error {
|
||||
@@ -833,8 +885,21 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
|
||||
func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
title: gui.Tr.CustomCommand,
|
||||
findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(),
|
||||
handleConfirm: func(command string) error {
|
||||
gui.Config.GetAppState().CustomCommandsHistory = utils.Limit(
|
||||
utils.Uniq(
|
||||
append(gui.Config.GetAppState().CustomCommandsHistory, command),
|
||||
),
|
||||
1000,
|
||||
)
|
||||
|
||||
err := gui.Config.SaveAppState()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
|
||||
@@ -8,11 +8,20 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileManagerDisplayFilter int
|
||||
|
||||
const (
|
||||
DisplayAll FileManagerDisplayFilter = iota
|
||||
DisplayStaged
|
||||
DisplayUnstaged
|
||||
)
|
||||
|
||||
type FileManager struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileManagerDisplayFilter
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
@@ -22,6 +31,7 @@ func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *Fil
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
filter: DisplayAll,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
@@ -35,6 +45,35 @@ func (m *FileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) GetFilesForDisplay() []*models.File {
|
||||
files := m.files
|
||||
if m.filter == DisplayAll {
|
||||
return files
|
||||
}
|
||||
|
||||
result := make([]*models.File, 0)
|
||||
if m.filter == DisplayStaged {
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, file := range files {
|
||||
if !file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *FileManager) SetDisplayFilter(filter FileManagerDisplayFilter) {
|
||||
m.filter = filter
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
@@ -73,10 +112,11 @@ func (m *FileManager) SetFiles(files []*models.File) {
|
||||
}
|
||||
|
||||
func (m *FileManager) SetTree() {
|
||||
filesForDisplay := m.GetFilesForDisplay()
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromFiles(m.files)
|
||||
m.tree = BuildTreeFromFiles(filesForDisplay)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromFiles(m.files)
|
||||
m.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
func init() {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
@@ -93,3 +95,62 @@ func TestRender(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAction(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
filter FileManagerDisplayFilter
|
||||
files []*models.File
|
||||
expected []*models.File
|
||||
}{
|
||||
{
|
||||
name: "filter files with unstaged changes",
|
||||
filter: DisplayUnstaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter files with staged changes",
|
||||
filter: DisplayStaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter all files",
|
||||
filter: DisplayAll,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileManager{files: s.files, filter: s.filter}
|
||||
result := mngr.GetFilesForDisplay()
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
|
||||
displayString: gui.Tr.LcFilterPathOption,
|
||||
onPress: func() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnterFileName,
|
||||
findSuggestionsFunc: gui.getFilePathSuggestionsFunc(),
|
||||
title: gui.Tr.EnterFileName,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.setFiltering(strings.TrimSpace(response))
|
||||
},
|
||||
|
||||
190
pkg/gui/find_suggestions.go
Normal file
190
pkg/gui/find_suggestions.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/minimal/gitignore"
|
||||
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
|
||||
)
|
||||
|
||||
// Thinking out loud: I'm typically a staunch advocate of organising code by feature rather than type,
|
||||
// because colocating code that relates to the same feature means far less effort
|
||||
// to get all the context you need to work on any particular feature. But the one
|
||||
// major benefit of grouping by type is that it makes it makes it less likely that
|
||||
// somebody will re-implement the same logic twice, because they can quickly see
|
||||
// if a certain method has been used for some use case, given that as a starting point
|
||||
// they know about the type. In that vein, I'm including all our functions for
|
||||
// finding suggestions in this file, so that it's easy to see if a function already
|
||||
// exists for fetching a particular model.
|
||||
|
||||
func (gui *Gui) getRemoteNames() []string {
|
||||
result := make([]string, len(gui.State.Remotes))
|
||||
for i, remote := range gui.State.Remotes {
|
||||
result[i] = remote.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func matchesToSuggestions(matches []string) []*types.Suggestion {
|
||||
suggestions := make([]*types.Suggestion, len(matches))
|
||||
for i, match := range matches {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: match,
|
||||
Label: match,
|
||||
}
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
remoteNames := gui.getRemoteNames()
|
||||
|
||||
return fuzzySearchFunc(remoteNames)
|
||||
}
|
||||
|
||||
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) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
branchNames := gui.getBranchNames()
|
||||
|
||||
return func(input string) []*types.Suggestion {
|
||||
var matchingBranchNames []string
|
||||
if input == "" {
|
||||
matchingBranchNames = branchNames
|
||||
} else {
|
||||
matchingBranchNames = utils.FuzzySearch(input, branchNames)
|
||||
}
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
|
||||
for i, branchName := range matchingBranchNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: branchName,
|
||||
Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName),
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
}
|
||||
|
||||
// here we asynchronously fetch the latest set of paths in the repo and store in
|
||||
// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
|
||||
// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
|
||||
// the old trie and eventually it'll be swapped out for the new one.
|
||||
// Notably, unlike other suggestion functions we're not showing all the options
|
||||
// if nothing has been typed because there'll be too much to display efficiently
|
||||
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
_ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
|
||||
trie := patricia.NewTrie()
|
||||
// load every non-gitignored file in the repo
|
||||
ignore, err := gitignore.FromGit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ignore.Walk(".",
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trie.Insert(patricia.Prefix(path), path)
|
||||
return nil
|
||||
})
|
||||
// cache the trie for future use
|
||||
gui.State.FilesTrie = trie
|
||||
|
||||
// refresh the selections view
|
||||
gui.suggestionsAsyncHandler.Do(func() func() {
|
||||
// assuming here that the confirmation view is what we're typing into.
|
||||
// This assumption may prove false over time
|
||||
suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
|
||||
return func() { gui.setSuggestions(suggestions) }
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return func(input string) []*types.Suggestion {
|
||||
matchingNames := []string{}
|
||||
_ = gui.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
|
||||
matchingNames = append(matchingNames, item.(string))
|
||||
return nil
|
||||
})
|
||||
|
||||
// doing another fuzzy search for good measure
|
||||
matchingNames = utils.FuzzySearch(input, matchingNames)
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingNames))
|
||||
for i, name := range matchingNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: name,
|
||||
Label: name,
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteBranchNames(separator string) []string {
|
||||
result := []string{}
|
||||
for _, remote := range gui.State.Remotes {
|
||||
for _, branch := range remote.Branches {
|
||||
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
|
||||
return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
|
||||
}
|
||||
|
||||
func (gui *Gui) getTagNames() []string {
|
||||
result := make([]string, len(gui.State.Tags))
|
||||
for i, tag := range gui.State.Tags {
|
||||
result[i] = tag.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
remoteBranchNames := gui.getRemoteBranchNames("/")
|
||||
localBranchNames := gui.getBranchNames()
|
||||
tagNames := gui.getTagNames()
|
||||
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
|
||||
|
||||
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
|
||||
|
||||
return fuzzySearchFunc(refNames)
|
||||
}
|
||||
|
||||
func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
|
||||
// reversing so that we display the latest command first
|
||||
history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
|
||||
|
||||
return fuzzySearchFunc(history)
|
||||
}
|
||||
|
||||
func fuzzySearchFunc(options []string) func(string) []*types.Suggestion {
|
||||
return func(input string) []*types.Suggestion {
|
||||
var matches []string
|
||||
if input == "" {
|
||||
matches = options
|
||||
} else {
|
||||
matches = utils.FuzzySearch(input, options)
|
||||
}
|
||||
|
||||
return matchesToSuggestions(matches)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const HORIZONTAL_SCROLL_FACTOR = 3
|
||||
|
||||
// these views need to be re-rendered when the screen mode changes. The commits view,
|
||||
// for example, will show authorship information in half and full screen mode.
|
||||
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
@@ -114,7 +116,7 @@ func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView(gui.Views.Main)
|
||||
@@ -122,12 +124,33 @@ func (gui *Gui) scrollUpMain() error {
|
||||
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeftMain() error {
|
||||
gui.scrollLeft(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRightMain() error {
|
||||
gui.scrollRight(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeft(view *gocui.View) {
|
||||
newOriginX := utils.Max(view.OriginX()-view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR, 0)
|
||||
_ = view.SetOriginX(newOriginX)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRight(view *gocui.View) {
|
||||
_ = view.SetOriginX(view.OriginX() + view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary() error {
|
||||
return gui.scrollUpView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
// Currently there is a bug where if we switch to a subprocess from within
|
||||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
@@ -19,23 +25,38 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
err := gui.OSCommand.RunCommand(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
|
||||
cmd.Env = append(cmd.Env, "TERM=dumb")
|
||||
cmdWriter := gui.getCmdWriter()
|
||||
cmd.Stdout = cmdWriter
|
||||
cmd.Stderr = cmdWriter
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return gui.surfaceError(
|
||||
fmt.Errorf(
|
||||
gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"os/exec"
|
||||
@@ -23,6 +22,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -30,8 +31,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
|
||||
)
|
||||
|
||||
// screen sizing determines how much space your selected window takes up (window
|
||||
@@ -51,10 +52,6 @@ const StartupPopupVersion = 5
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
func init() {
|
||||
runewidth.DefaultCondition.EastAsianWidth = false
|
||||
}
|
||||
|
||||
type ContextManager struct {
|
||||
ContextStack []Context
|
||||
sync.RWMutex
|
||||
@@ -122,6 +119,8 @@ type Gui struct {
|
||||
|
||||
// the extras window contains things like the command log
|
||||
ShowExtrasWindow bool
|
||||
|
||||
suggestionsAsyncHandler *tasks.AsyncHandler
|
||||
}
|
||||
|
||||
type listPanelState struct {
|
||||
@@ -147,9 +146,9 @@ type LblPanelState struct {
|
||||
type MergingPanelState struct {
|
||||
*mergeconflicts.State
|
||||
|
||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||
// UserVerticalScrolling tells us if the user has started scrolling through the file themselves
|
||||
// in which case we won't auto-scroll to a conflict.
|
||||
UserScrolling bool
|
||||
UserVerticalScrolling bool
|
||||
}
|
||||
|
||||
type filePanelState struct {
|
||||
@@ -316,6 +315,8 @@ type guiState struct {
|
||||
RetainOriginalDir bool
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
// if this is true, we'll load our commits using `git log --all`
|
||||
ShowWholeGitGraph bool
|
||||
ScreenMode WindowMaximisation
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
@@ -342,6 +343,9 @@ type guiState struct {
|
||||
|
||||
// flag as to whether or not the diff view should ignore whitespace
|
||||
IgnoreWhitespaceInDiffView bool
|
||||
|
||||
// for displaying suggestions while typing in a file name
|
||||
FilesTrie *patricia.Trie
|
||||
}
|
||||
|
||||
// reuseState determines if we pull the repo state from our repo state map or
|
||||
@@ -394,7 +398,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
|
||||
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
|
||||
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
|
||||
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
|
||||
@@ -402,8 +406,8 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||
Merging: &MergingPanelState{
|
||||
State: mergeconflicts.NewState(),
|
||||
UserScrolling: false,
|
||||
State: mergeconflicts.NewState(),
|
||||
UserVerticalScrolling: false,
|
||||
},
|
||||
},
|
||||
Ptmx: nil,
|
||||
@@ -418,6 +422,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
// TODO: put contexts in the context manager
|
||||
ContextManager: NewContextManager(initialContext),
|
||||
Contexts: contexts,
|
||||
FilesTrie: patricia.NewTrie(),
|
||||
}
|
||||
|
||||
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
||||
@@ -427,19 +432,20 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
|
||||
gui := &Gui{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
|
||||
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
|
||||
}
|
||||
|
||||
gui.resetState(filterPath, false)
|
||||
@@ -450,9 +456,17 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
oSCommand.SetOnRunCommand(onRunCommand)
|
||||
gui.OnRunCommand = onRunCommand
|
||||
|
||||
authors.SetCustomAuthors(gui.Config.GetUserConfig().Gui.AuthorColors)
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
var RuneReplacements = map[rune]string{
|
||||
// for the commit graph
|
||||
graph.MergeSymbol: "M",
|
||||
graph.CommitSymbol: "o",
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
recordEvents := recordingEvents()
|
||||
@@ -463,7 +477,7 @@ func (gui *Gui) Run() error {
|
||||
playMode = gocui.REPLAYING
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless(), RuneReplacements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -497,8 +511,6 @@ func (gui *Gui) Run() error {
|
||||
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
|
||||
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
|
||||
|
||||
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
|
||||
|
||||
g.ShowListFooter = userConfig.Gui.ShowListFooter
|
||||
|
||||
if userConfig.Gui.MouseEvents {
|
||||
@@ -709,6 +721,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
} else {
|
||||
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
|
||||
err := gui.fetch(false, "")
|
||||
gui.render()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package gui
|
||||
@@ -55,8 +56,8 @@ func Test(t *testing.T) {
|
||||
updateSnapshots,
|
||||
record,
|
||||
speedEnv,
|
||||
func(t *testing.T, expected string, actual string) {
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
|
||||
func(t *testing.T, expected string, actual string, prefix string) {
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
|
||||
},
|
||||
includeSkipped,
|
||||
)
|
||||
@@ -80,59 +81,3 @@ func runCmdHeadless(cmd *exec.Cmd) error {
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
cmdOut string
|
||||
filter string
|
||||
valueFormat string
|
||||
labelFormat string
|
||||
test func([]commandMenuEntry, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Extract remote branch name",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
|
||||
"{{ .branch }}",
|
||||
"Remote: {{ .remote }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with empty labelFormat",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .branch }}|{{ .remote }}",
|
||||
"",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with group ids",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .group_2 }}|{{ .group_1 }}",
|
||||
"Remote: {{ .group_1 }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleShowAllBranchLogs,
|
||||
Description: gui.Tr.LcAllBranchesLogGraph,
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Key: gui.getKey("<c-b>"),
|
||||
Handler: gui.handleStatusFilterPressed,
|
||||
Description: gui.Tr.LcCommitFileFilter,
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{string(FILES_CONTEXT_KEY)},
|
||||
@@ -718,6 +724,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleFetchRemote,
|
||||
Description: gui.Tr.LcFetchRemote,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Commits.OpenLogMenu),
|
||||
Handler: gui.handleOpenLogMenu,
|
||||
Description: gui.Tr.LcOpenLogMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
@@ -1297,11 +1311,19 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.CopyToClipboard),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.copySelectedToClipboard,
|
||||
Description: gui.Tr.LcCopySelectedTexToClipboard,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_STAGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.Edit),
|
||||
Handler: gui.handleFileEdit,
|
||||
Handler: gui.handleLineByLineEdit,
|
||||
Description: gui.Tr.LcEditFile,
|
||||
},
|
||||
{
|
||||
@@ -1412,6 +1434,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.ScrollLeft),
|
||||
Handler: gui.scrollLeftMain,
|
||||
Description: gui.Tr.LcScrollLeft,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.ScrollRight),
|
||||
Handler: gui.scrollRightMain,
|
||||
Description: gui.Tr.LcScrollRight,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_STAGING_CONTEXT_KEY)},
|
||||
@@ -1458,8 +1494,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Main.PickBothHunks),
|
||||
Handler: gui.handlePickBothHunks,
|
||||
Description: gui.Tr.PickBothHunks,
|
||||
Handler: gui.handlePickAllHunks,
|
||||
Description: gui.Tr.PickAllHunks,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
@@ -1479,29 +1515,29 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.PrevItem),
|
||||
Handler: gui.handleSelectTop,
|
||||
Description: gui.Tr.SelectTop,
|
||||
Handler: gui.handleSelectPrevConflictHunk,
|
||||
Description: gui.Tr.SelectPrevHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.NextItem),
|
||||
Handler: gui.handleSelectBottom,
|
||||
Description: gui.Tr.SelectBottom,
|
||||
Handler: gui.handleSelectNextConflictHunk,
|
||||
Description: gui.Tr.SelectNextHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
Handler: gui.handleSelectPrevConflictHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
Handler: gui.handleSelectNextConflictHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
@@ -1522,14 +1558,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.PrevItemAlt),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
Handler: gui.handleSelectPrevConflictHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.NextItemAlt),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
Handler: gui.handleSelectNextConflictHunk,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
@@ -1804,8 +1840,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
}
|
||||
|
||||
// Appends keybindings to jump to a particular sideView using numbers
|
||||
for i, window := range []string{"status", "files", "branches", "commits", "stash"} {
|
||||
bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideWindow(window)})
|
||||
windows := []string{"status", "files", "branches", "commits", "stash"}
|
||||
|
||||
if len(config.Universal.JumpToBlock) != len(windows) {
|
||||
log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.")
|
||||
} else {
|
||||
for i, window := range windows {
|
||||
bindings = append(bindings, &Binding{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.JumpToBlock[i]),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.goToSideWindow(window)})
|
||||
}
|
||||
}
|
||||
|
||||
for viewName := range gui.State.Contexts.initialViewTabContextMap() {
|
||||
|
||||
@@ -52,24 +52,19 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
gui.Views.Stash.Title = gui.Tr.StashTitle
|
||||
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Stash.ContainsList = true
|
||||
|
||||
gui.Views.Commits.Title = gui.Tr.CommitsTitle
|
||||
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Commits.ContainsList = true
|
||||
|
||||
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
|
||||
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitFiles.ContainsList = true
|
||||
|
||||
gui.Views.Branches.Title = gui.Tr.BranchesTitle
|
||||
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Branches.ContainsList = true
|
||||
|
||||
gui.Views.Files.Highlight = true
|
||||
gui.Views.Files.Title = gui.Tr.FilesTitle
|
||||
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Files.ContainsList = true
|
||||
|
||||
gui.Views.Secondary.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Secondary.Wrap = true
|
||||
@@ -122,12 +117,15 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
gui.Views.Extras.Wrap = true
|
||||
|
||||
gui.printCommandLogHeader()
|
||||
|
||||
if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.GitCommand.GetCmdWriter = gui.getCmdWriter
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -260,7 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
}
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
view, err := gui.g.View(listContext.ViewName)
|
||||
view, err := gui.g.View(listContext.GetViewName())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -270,8 +268,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
view.FocusPoint(0, listContext.GetPanelState().GetSelectedLineIdx())
|
||||
listContext.FocusLine()
|
||||
|
||||
view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
|
||||
|
||||
@@ -180,6 +180,11 @@ func (s *State) RenderForLineIndices(includedLineIndices []int) string {
|
||||
return s.patchParser.Render(firstLineIdx, lastLineIdx, includedLineIndices)
|
||||
}
|
||||
|
||||
func (s *State) PlainRenderSelected() string {
|
||||
firstLineIdx, lastLineIdx := s.SelectedRange()
|
||||
return s.patchParser.PlainRenderLines(firstLineIdx, lastLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) SelectBottom() {
|
||||
s.SetLineSelectMode()
|
||||
s.SelectLine(len(s.patchParser.PatchLines) - 1)
|
||||
|
||||
@@ -86,6 +86,20 @@ func (gui *Gui) handleSelectNextHunk() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) copySelectedToClipboard() error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
selected := state.PlainRenderSelected()
|
||||
|
||||
if err := gui.OSCommand.WithSpan(
|
||||
gui.Tr.Spans.CopySelectedTextToClipboard,
|
||||
).CopyToClipboard(selected); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshAndFocusLblPanel(state *LblPanelState) error {
|
||||
if err := gui.refreshMainViewForLineByLine(state); err != nil {
|
||||
return err
|
||||
@@ -160,7 +174,7 @@ func (gui *Gui) focusSelection(state *LblPanelState) error {
|
||||
newOrigin := state.CalculateOrigin(origin, bufferHeight)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
|
||||
if err := stagingView.SetOriginY(newOrigin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -272,3 +286,13 @@ func (gui *Gui) withLBLActiveCheck(f func(*LblPanelState) error) error {
|
||||
|
||||
return f(state)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLineByLineEdit() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lineNumber := gui.State.Panels.LineByLine.CurrentLineNumber()
|
||||
return gui.editFileAtLine(file.Name, lineNumber)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,57 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type ListContext struct {
|
||||
GetItemsLength func() int
|
||||
GetDisplayStrings func() [][]string
|
||||
GetDisplayStrings func(startIdx int, length int) [][]string
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnClickSelectedItem func() error
|
||||
|
||||
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
|
||||
SelectedItem func() (ListItem, bool)
|
||||
GetPanelState func() IListPanelState
|
||||
SelectedItem func() (ListItem, bool)
|
||||
OnGetPanelState func() IListPanelState
|
||||
// if this is true, we'll call GetDisplayStrings for just the visible part of the
|
||||
// view and re-render that. This is useful when you need to render different
|
||||
// content based on the selection (e.g. for showing the selected commit)
|
||||
RenderSelection bool
|
||||
|
||||
Gui *Gui
|
||||
ResetMainViewOriginOnFocus bool
|
||||
Gui *Gui
|
||||
|
||||
*BasicContext
|
||||
}
|
||||
|
||||
type IListContext interface {
|
||||
GetSelectedItem() (ListItem, bool)
|
||||
GetSelectedItemId() string
|
||||
OnRender() error
|
||||
handlePrevLine() error
|
||||
handleNextLine() error
|
||||
handleScrollLeft() error
|
||||
handleScrollRight() error
|
||||
handleLineChange(change int) error
|
||||
handleNextPage() error
|
||||
handleGotoTop() error
|
||||
handleGotoBottom() error
|
||||
handlePrevPage() error
|
||||
handleClick() error
|
||||
onSearchSelect(selectedLineIdx int) error
|
||||
FocusLine()
|
||||
|
||||
GetPanelState() IListPanelState
|
||||
|
||||
Context
|
||||
}
|
||||
|
||||
func (self *ListContext) GetPanelState() IListPanelState {
|
||||
return self.OnGetPanelState()
|
||||
}
|
||||
|
||||
type IListPanelState interface {
|
||||
SetSelectedLineIdx(int)
|
||||
GetSelectedLineIdx() int
|
||||
@@ -30,12 +65,33 @@ type ListItem interface {
|
||||
Description() string
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
|
||||
return lc.SelectedItem()
|
||||
func (self *ListContext) FocusLine() {
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
// ignoring error for now
|
||||
return
|
||||
}
|
||||
|
||||
// we need a way of knowing whether we've rendered to the view yet.
|
||||
view.FocusPoint(view.OriginX(), self.GetPanelState().GetSelectedLineIdx())
|
||||
if self.RenderSelection {
|
||||
_, originY := view.Origin()
|
||||
displayStrings := self.GetDisplayStrings(originY, view.InnerHeight())
|
||||
self.Gui.renderDisplayStringsAtPos(view, originY, displayStrings)
|
||||
}
|
||||
view.Footer = formatListFooter(self.GetPanelState().GetSelectedLineIdx(), self.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetSelectedItemId() string {
|
||||
item, ok := lc.SelectedItem()
|
||||
func formatListFooter(selectedLineIdx int, length int) string {
|
||||
return fmt.Sprintf("%d of %d", selectedLineIdx+1, length)
|
||||
}
|
||||
|
||||
func (self *ListContext) GetSelectedItem() (ListItem, bool) {
|
||||
return self.SelectedItem()
|
||||
}
|
||||
|
||||
func (self *ListContext) GetSelectedItemId() string {
|
||||
item, ok := self.GetSelectedItem()
|
||||
|
||||
if !ok {
|
||||
return ""
|
||||
@@ -45,154 +101,170 @@ func (lc *ListContext) GetSelectedItemId() string {
|
||||
}
|
||||
|
||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||
func (lc *ListContext) OnRender() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
func (self *ListContext) OnRender() error {
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lc.GetDisplayStrings != nil {
|
||||
lc.Gui.refreshSelectedLine(lc.GetPanelState(), lc.GetItemsLength())
|
||||
lc.Gui.renderDisplayStrings(view, lc.GetDisplayStrings())
|
||||
if self.GetDisplayStrings != nil {
|
||||
self.Gui.refreshSelectedLine(self.GetPanelState(), self.GetItemsLength())
|
||||
self.Gui.renderDisplayStrings(view, self.GetDisplayStrings(0, self.GetItemsLength()))
|
||||
self.Gui.render()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleFocusLost() error {
|
||||
if lc.OnFocusLost != nil {
|
||||
return lc.OnFocusLost()
|
||||
func (self *ListContext) HandleFocusLost() error {
|
||||
if self.OnFocusLost != nil {
|
||||
return self.OnFocusLost()
|
||||
}
|
||||
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = view.SetOriginX(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *ListContext) HandleFocus() error {
|
||||
if self.Gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.FocusLine()
|
||||
|
||||
if self.Gui.State.Modes.Diffing.Active() {
|
||||
return self.Gui.renderDiff()
|
||||
}
|
||||
|
||||
if self.OnFocus != nil {
|
||||
return self.OnFocus()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleFocus() error {
|
||||
if lc.Gui.popupPanelFocused() {
|
||||
func (self *ListContext) HandleRender() error {
|
||||
return self.OnRender()
|
||||
}
|
||||
|
||||
func (self *ListContext) handlePrevLine() error {
|
||||
return self.handleLineChange(-1)
|
||||
}
|
||||
|
||||
func (self *ListContext) handleNextLine() error {
|
||||
return self.handleLineChange(1)
|
||||
}
|
||||
|
||||
func (self *ListContext) handleScrollLeft() error {
|
||||
return self.scroll(self.Gui.scrollLeft)
|
||||
}
|
||||
|
||||
func (self *ListContext) handleScrollRight() error {
|
||||
return self.scroll(self.Gui.scrollRight)
|
||||
}
|
||||
|
||||
func (self *ListContext) scroll(scrollFunc func(*gocui.View)) error {
|
||||
if self.ignoreKeybinding() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
// get the view, move the origin
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
scrollFunc(view)
|
||||
|
||||
if lc.ResetMainViewOriginOnFocus {
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if lc.Gui.State.Modes.Diffing.Active() {
|
||||
return lc.Gui.renderDiff()
|
||||
}
|
||||
|
||||
if lc.OnFocus != nil {
|
||||
return lc.OnFocus()
|
||||
}
|
||||
|
||||
return nil
|
||||
return self.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleRender() error {
|
||||
return lc.OnRender()
|
||||
func (self *ListContext) ignoreKeybinding() bool {
|
||||
return !self.Gui.isPopupPanel(self.ViewName) && self.Gui.popupPanelFocused()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevLine() error {
|
||||
return lc.handleLineChange(-1)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextLine() error {
|
||||
return lc.handleLineChange(1)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleLineChange(change int) error {
|
||||
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
|
||||
func (self *ListContext) handleLineChange(change int) error {
|
||||
if self.ignoreKeybinding() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
|
||||
if (change < 0 && selectedLineIdx == 0) || (change > 0 && selectedLineIdx == lc.GetItemsLength()-1) {
|
||||
selectedLineIdx := self.GetPanelState().GetSelectedLineIdx()
|
||||
if (change < 0 && selectedLineIdx == 0) || (change > 0 && selectedLineIdx == self.GetItemsLength()-1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
lc.Gui.changeSelectedLine(lc.GetPanelState(), lc.GetItemsLength(), change)
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
self.Gui.changeSelectedLine(self.GetPanelState(), self.GetItemsLength(), change)
|
||||
|
||||
return lc.HandleFocus()
|
||||
return self.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextPage() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
func (self *ListContext) handleNextPage() error {
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
delta := lc.Gui.pageDelta(view)
|
||||
delta := self.Gui.pageDelta(view)
|
||||
|
||||
return lc.handleLineChange(delta)
|
||||
return self.handleLineChange(delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoTop() error {
|
||||
return lc.handleLineChange(-lc.GetItemsLength())
|
||||
func (self *ListContext) handleGotoTop() error {
|
||||
return self.handleLineChange(-self.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoBottom() error {
|
||||
return lc.handleLineChange(lc.GetItemsLength())
|
||||
func (self *ListContext) handleGotoBottom() error {
|
||||
return self.handleLineChange(self.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevPage() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
func (self *ListContext) handlePrevPage() error {
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
delta := lc.Gui.pageDelta(view)
|
||||
delta := self.Gui.pageDelta(view)
|
||||
|
||||
return lc.handleLineChange(-delta)
|
||||
return self.handleLineChange(-delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleClick() error {
|
||||
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
|
||||
func (self *ListContext) handleClick() error {
|
||||
if self.ignoreKeybinding() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
view, err := self.Gui.g.View(self.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
|
||||
prevSelectedLineIdx := self.GetPanelState().GetSelectedLineIdx()
|
||||
newSelectedLineIdx := view.SelectedLineIdx()
|
||||
|
||||
// we need to focus the view
|
||||
if err := lc.Gui.pushContext(lc); err != nil {
|
||||
if err := self.Gui.pushContext(self); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newSelectedLineIdx > lc.GetItemsLength()-1 {
|
||||
if newSelectedLineIdx > self.GetItemsLength()-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lc.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
|
||||
self.GetPanelState().SetSelectedLineIdx(newSelectedLineIdx)
|
||||
|
||||
prevViewName := lc.Gui.currentViewName()
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lc.ViewName && lc.OnClickSelectedItem != nil {
|
||||
return lc.OnClickSelectedItem()
|
||||
prevViewName := self.Gui.currentViewName()
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == self.ViewName && self.OnClickSelectedItem != nil {
|
||||
return self.OnClickSelectedItem()
|
||||
}
|
||||
return lc.HandleFocus()
|
||||
return self.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
|
||||
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
|
||||
return lc.HandleFocus()
|
||||
func (self *ListContext) onSearchSelect(selectedLineIdx int) error {
|
||||
self.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
|
||||
return self.HandleFocus()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
func (gui *Gui) menuListContext() *ListContext {
|
||||
func (gui *Gui) menuListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "menu",
|
||||
@@ -14,18 +16,17 @@ func (gui *Gui) menuListContext() *ListContext {
|
||||
Kind: PERSISTENT_POPUP,
|
||||
OnGetOptionsMap: gui.getMenuOptions,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: gui.onMenuPress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: gui.onMenuPress,
|
||||
Gui: gui,
|
||||
|
||||
// no GetDisplayStrings field because we do a custom render on menu creation
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) filesListContext() *ListContext {
|
||||
func (gui *Gui) filesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "files",
|
||||
@@ -33,13 +34,12 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
Key: FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
@@ -55,7 +55,7 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchesListContext() *ListContext {
|
||||
func (gui *Gui) branchesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
@@ -63,12 +63,11 @@ func (gui *Gui) branchesListContext() *ListContext {
|
||||
Key: LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Branches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
|
||||
OnFocus: gui.handleBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Branches) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
|
||||
OnFocus: gui.handleBranchSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -78,7 +77,7 @@ func (gui *Gui) branchesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remotesListContext() *ListContext {
|
||||
func (gui *Gui) remotesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
@@ -86,13 +85,12 @@ func (gui *Gui) remotesListContext() *ListContext {
|
||||
Key: REMOTES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Remotes) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
|
||||
OnFocus: gui.handleRemoteSelect,
|
||||
OnClickSelectedItem: gui.handleRemoteEnter,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Remotes) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
|
||||
OnFocus: gui.handleRemoteSelect,
|
||||
OnClickSelectedItem: gui.handleRemoteEnter,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -102,7 +100,7 @@ func (gui *Gui) remotesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
func (gui *Gui) remoteBranchesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
@@ -110,12 +108,11 @@ func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
Key: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
|
||||
OnFocus: gui.handleRemoteBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
|
||||
OnFocus: gui.handleRemoteBranchSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -125,7 +122,7 @@ func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) tagsListContext() *ListContext {
|
||||
func (gui *Gui) tagsListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
@@ -133,12 +130,11 @@ func (gui *Gui) tagsListContext() *ListContext {
|
||||
Key: TAGS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Tags) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
|
||||
OnFocus: gui.handleTagSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Tags) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
|
||||
OnFocus: gui.handleTagSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -148,7 +144,7 @@ func (gui *Gui) tagsListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchCommitsListContext() *ListContext {
|
||||
func (gui *Gui) branchCommitsListContext() IListContext {
|
||||
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
@@ -157,29 +153,96 @@ func (gui *Gui) branchCommitsListContext() *ListContext {
|
||||
Key: BRANCH_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Commits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
|
||||
OnFocus: gui.handleCommitSelect,
|
||||
OnClickSelectedItem: gui.handleViewCommitFiles,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Commits) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
|
||||
OnFocus: gui.handleCommitSelect,
|
||||
OnClickSelectedItem: gui.handleViewCommitFiles,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
selectedCommitSha := ""
|
||||
if gui.currentContext().GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
selectedCommit := gui.getSelectedLocalCommit()
|
||||
if selectedCommit != nil {
|
||||
selectedCommitSha = selectedCommit.Sha
|
||||
}
|
||||
}
|
||||
return presentation.GetCommitListDisplayStrings(
|
||||
gui.State.Commits,
|
||||
gui.State.ScreenMode != SCREEN_NORMAL,
|
||||
gui.cherryPickedCommitShaMap(),
|
||||
gui.State.Modes.Diffing.Ref,
|
||||
parseEmoji,
|
||||
selectedCommitSha,
|
||||
startIdx,
|
||||
length,
|
||||
gui.shouldShowGraph(),
|
||||
)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedLocalCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
RenderSelection: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
func (gui *Gui) subCommitsListContext() IListContext {
|
||||
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: SUB_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.SubCommits) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
|
||||
OnFocus: gui.handleSubCommitSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
selectedCommitSha := ""
|
||||
if gui.currentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY {
|
||||
selectedCommit := gui.getSelectedSubCommit()
|
||||
if selectedCommit != nil {
|
||||
selectedCommitSha = selectedCommit.Sha
|
||||
}
|
||||
}
|
||||
return presentation.GetCommitListDisplayStrings(
|
||||
gui.State.SubCommits,
|
||||
gui.State.ScreenMode != SCREEN_NORMAL,
|
||||
gui.cherryPickedCommitShaMap(),
|
||||
gui.State.Modes.Diffing.Ref,
|
||||
parseEmoji,
|
||||
selectedCommitSha,
|
||||
startIdx,
|
||||
length,
|
||||
gui.shouldShowGraph(),
|
||||
)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
RenderSelection: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) shouldShowGraph() bool {
|
||||
value := gui.Config.GetUserConfig().Git.Log.ShowGraph
|
||||
switch value {
|
||||
case "always":
|
||||
return true
|
||||
case "never":
|
||||
return false
|
||||
case "when-maximised":
|
||||
return gui.State.ScreenMode != SCREEN_NORMAL
|
||||
}
|
||||
|
||||
log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value)
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) reflogCommitsListContext() IListContext {
|
||||
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
@@ -188,12 +251,11 @@ func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
Key: REFLOG_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
|
||||
OnFocus: gui.handleReflogCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
|
||||
OnFocus: gui.handleReflogCommitSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetReflogCommitListDisplayStrings(
|
||||
gui.State.FilteredReflogCommits,
|
||||
gui.State.ScreenMode != SCREEN_NORMAL,
|
||||
@@ -209,37 +271,7 @@ func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) subCommitsListContext() *ListContext {
|
||||
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: SUB_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.SubCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
|
||||
OnFocus: gui.handleSubCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(
|
||||
gui.State.SubCommits,
|
||||
gui.State.ScreenMode != SCREEN_NORMAL,
|
||||
gui.cherryPickedCommitShaMap(),
|
||||
gui.State.Modes.Diffing.Ref,
|
||||
parseEmoji,
|
||||
)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) stashListContext() *ListContext {
|
||||
func (gui *Gui) stashListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "stash",
|
||||
@@ -247,12 +279,11 @@ func (gui *Gui) stashListContext() *ListContext {
|
||||
Key: STASH_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.StashEntries) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
|
||||
OnFocus: gui.handleStashEntrySelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.StashEntries) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
|
||||
OnFocus: gui.handleStashEntrySelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -262,7 +293,7 @@ func (gui *Gui) stashListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
func (gui *Gui) commitFilesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "commitFiles",
|
||||
@@ -270,12 +301,11 @@ func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
Key: COMMIT_FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
if gui.State.CommitFileManager.GetItemsLength() == 0 {
|
||||
return [][]string{{style.FgRed.Sprint("(none)")}}
|
||||
}
|
||||
@@ -295,7 +325,7 @@ func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) submodulesListContext() *ListContext {
|
||||
func (gui *Gui) submodulesListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "files",
|
||||
@@ -303,12 +333,11 @@ func (gui *Gui) submodulesListContext() *ListContext {
|
||||
Key: SUBMODULES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Submodules) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
|
||||
OnFocus: gui.handleSubmoduleSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Submodules) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
|
||||
OnFocus: gui.handleSubmoduleSelect,
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
@@ -318,7 +347,7 @@ func (gui *Gui) submodulesListContext() *ListContext {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) suggestionsListContext() *ListContext {
|
||||
func (gui *Gui) suggestionsListContext() IListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "suggestions",
|
||||
@@ -326,19 +355,18 @@ func (gui *Gui) suggestionsListContext() *ListContext {
|
||||
Key: SUGGESTIONS_CONTEXT_KEY,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
},
|
||||
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,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
GetItemsLength: func() int { return len(gui.State.Suggestions) },
|
||||
OnGetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
|
||||
OnFocus: func() error { return nil },
|
||||
Gui: gui,
|
||||
GetDisplayStrings: func(startIdx int, length int) [][]string {
|
||||
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContexts() []*ListContext {
|
||||
return []*ListContext{
|
||||
func (gui *Gui) getListContexts() []IListContext {
|
||||
return []IListContext{
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Branches,
|
||||
@@ -346,7 +374,6 @@ func (gui *Gui) getListContexts() []*ListContext {
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
@@ -365,38 +392,40 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
listContext := listContext
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.Key)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.GetViewName(), Contexts: []string{string(listContext.GetKey())}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollLeft), Modifier: gocui.ModNone, Handler: listContext.handleScrollLeft},
|
||||
{ViewName: listContext.GetViewName(), Tag: "navigation", Contexts: []string{string(listContext.GetKey())}, Key: gui.getKey(keybindingConfig.Universal.ScrollRight), Modifier: gocui.ModNone, Handler: listContext.handleScrollRight},
|
||||
}...)
|
||||
|
||||
// the commits panel needs to lazyload things so it has a couple of its own handlers
|
||||
openSearchHandler := gui.handleOpenSearch
|
||||
gotoBottomHandler := listContext.handleGotoBottom
|
||||
if listContext.ViewName == "commits" {
|
||||
if listContext.GetViewName() == "commits" {
|
||||
openSearchHandler = gui.handleOpenSearchForCommitsPanel
|
||||
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
|
||||
}
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.Key)},
|
||||
ViewName: listContext.GetViewName(),
|
||||
Contexts: []string{string(listContext.GetKey())},
|
||||
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
|
||||
Handler: func() error { return openSearchHandler(listContext.ViewName) },
|
||||
Handler: func() error { return openSearchHandler(listContext.GetViewName()) },
|
||||
Description: gui.Tr.LcStartSearch,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.Key)},
|
||||
ViewName: listContext.GetViewName(),
|
||||
Contexts: []string{string(listContext.GetKey())},
|
||||
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
|
||||
Handler: gotoBottomHandler,
|
||||
Description: gui.Tr.LcGotoBottom,
|
||||
|
||||
@@ -29,7 +29,6 @@ type TaskKind int
|
||||
const (
|
||||
RENDER_STRING TaskKind = iota
|
||||
RENDER_STRING_WITHOUT_SCROLL
|
||||
RUN_FUNCTION
|
||||
RUN_COMMAND
|
||||
RUN_PTY
|
||||
)
|
||||
@@ -97,19 +96,6 @@ func NewRunPtyTask(cmd *exec.Cmd) *runPtyTask {
|
||||
// return &runPtyTask{cmd: cmd, prefix: prefix}
|
||||
// }
|
||||
|
||||
type runFunctionTask struct {
|
||||
f func(chan struct{}) error
|
||||
}
|
||||
|
||||
func (t *runFunctionTask) GetKind() TaskKind {
|
||||
return RUN_FUNCTION
|
||||
}
|
||||
|
||||
// currently unused
|
||||
// func (gui *Gui) createRunFunctionTask(f func(chan struct{}) error) *runFunctionTask {
|
||||
// return &runFunctionTask{f: f}
|
||||
// }
|
||||
|
||||
func (gui *Gui) runTaskForView(view *gocui.View, task updateTask) error {
|
||||
switch task.GetKind() {
|
||||
case RENDER_STRING:
|
||||
@@ -120,10 +106,6 @@ func (gui *Gui) runTaskForView(view *gocui.View, task updateTask) error {
|
||||
specificTask := task.(*renderStringWithoutScrollTask)
|
||||
return gui.newStringTaskWithoutScroll(view, specificTask.str)
|
||||
|
||||
case RUN_FUNCTION:
|
||||
specificTask := task.(*runFunctionTask)
|
||||
return gui.newTask(view, specificTask.f)
|
||||
|
||||
case RUN_COMMAND:
|
||||
specificTask := task.(*runCommandTask)
|
||||
return gui.newCmdTask(view, specificTask.cmd, specificTask.prefix)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -13,6 +14,8 @@ type menuItem struct {
|
||||
displayString string
|
||||
displayStrings []string
|
||||
onPress func() error
|
||||
// only applies when displayString is used
|
||||
opensMenu bool
|
||||
}
|
||||
|
||||
// every item in a list context needs an ID
|
||||
@@ -43,7 +46,7 @@ func (gui *Gui) getMenuOptions() map[string]string {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose() error {
|
||||
return gui.returnFromContext()
|
||||
return gui.returnFromContextSync()
|
||||
}
|
||||
|
||||
type createMenuOptions struct {
|
||||
@@ -65,8 +68,16 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
|
||||
stringArrays := make([][]string, len(items))
|
||||
for i, item := range items {
|
||||
if item.opensMenu && item.displayStrings != nil {
|
||||
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
|
||||
}
|
||||
|
||||
if item.displayStrings == nil {
|
||||
stringArrays[i] = []string{item.displayString}
|
||||
styledStr := item.displayString
|
||||
if item.opensMenu {
|
||||
styledStr = opensMenuStyle(styledStr)
|
||||
}
|
||||
stringArrays[i] = []string{styledStr}
|
||||
} else {
|
||||
stringArrays[i] = item.displayStrings
|
||||
}
|
||||
@@ -78,12 +89,10 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView.Title = title
|
||||
menuView.FgColor = theme.GocuiDefaultTextColor
|
||||
menuView.ContainsList = true
|
||||
menuView.Clear()
|
||||
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
|
||||
return nil
|
||||
}))
|
||||
fmt.Fprint(menuView, list)
|
||||
menuView.SetContent(list)
|
||||
gui.State.Panels.Menu.SelectedLineIdx = 0
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
|
||||
@@ -14,18 +14,18 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleSelectTop() error {
|
||||
func (gui *Gui) handleSelectPrevConflictHunk() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.SelectTopOption()
|
||||
gui.State.Panels.Merging.SelectPrevConflictHunk()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectBottom() error {
|
||||
func (gui *Gui) handleSelectNextConflictHunk() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.SelectBottomOption()
|
||||
gui.State.Panels.Merging.SelectNextConflictHunk()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
@@ -95,11 +95,11 @@ func (gui *Gui) handlePickHunk() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickBothHunks() error {
|
||||
func (gui *Gui) handlePickAllHunks() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
ok, err := gui.resolveConflict(mergeconflicts.BOTH)
|
||||
ok, err := gui.resolveConflict(mergeconflicts.ALL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,10 +135,12 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error
|
||||
switch selection {
|
||||
case mergeconflicts.TOP:
|
||||
logStr = "Picking top hunk"
|
||||
case mergeconflicts.MIDDLE:
|
||||
logStr = "Picking middle hunk"
|
||||
case mergeconflicts.BOTTOM:
|
||||
logStr = "Picking bottom hunk"
|
||||
case mergeconflicts.BOTH:
|
||||
logStr = "Picking both hunks"
|
||||
case mergeconflicts.ALL:
|
||||
logStr = "Picking all hunks"
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
|
||||
return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
@@ -201,7 +203,7 @@ func (gui *Gui) catSelectedFile() (string, error) {
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollToConflict() error {
|
||||
if gui.State.Panels.Merging.UserScrolling {
|
||||
if gui.State.Panels.Merging.UserVerticalScrolling {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -231,7 +233,7 @@ func (gui *Gui) getMergingOptions() map[string]string {
|
||||
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcSelectHunk,
|
||||
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.Tr.LcNavigateConflicts,
|
||||
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.Tr.LcPickHunk,
|
||||
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickBothHunks,
|
||||
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickAllHunks,
|
||||
gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.Tr.LcUndo,
|
||||
}
|
||||
}
|
||||
@@ -283,7 +285,7 @@ func (gui *Gui) promptToContinueRebase() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.genericMergeCommand("continue")
|
||||
return gui.genericMergeCommand(REBASE_OPTION_CONTINUE)
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.pushContext(gui.State.Contexts.Files)
|
||||
@@ -313,5 +315,5 @@ func (gui *Gui) withMergeConflictLock(f func() error) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) takeOverMergeConflictScrolling() {
|
||||
gui.State.Panels.Merging.UserScrolling = false
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = false
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ type LineType int
|
||||
|
||||
const (
|
||||
START LineType = iota
|
||||
MIDDLE
|
||||
ANCESTOR
|
||||
TARGET
|
||||
END
|
||||
NOT_A_MARKER
|
||||
)
|
||||
@@ -28,12 +29,20 @@ func findConflicts(content string) []*mergeConflict {
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
switch determineLineType(line) {
|
||||
case START:
|
||||
newConflict = &mergeConflict{start: i}
|
||||
case MIDDLE:
|
||||
newConflict.middle = i
|
||||
newConflict = &mergeConflict{start: i, ancestor: -1}
|
||||
case ANCESTOR:
|
||||
if newConflict != nil {
|
||||
newConflict.ancestor = i
|
||||
}
|
||||
case TARGET:
|
||||
if newConflict != nil {
|
||||
newConflict.target = i
|
||||
}
|
||||
case END:
|
||||
newConflict.end = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
if newConflict != nil {
|
||||
newConflict.end = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
// reset value to avoid any possible silent mutations in further iterations
|
||||
newConflict = nil
|
||||
default:
|
||||
@@ -50,8 +59,10 @@ func determineLineType(line string) LineType {
|
||||
switch {
|
||||
case strings.HasPrefix(trimmedLine, "<<<<<<< "):
|
||||
return START
|
||||
case strings.HasPrefix(trimmedLine, "||||||| "):
|
||||
return ANCESTOR
|
||||
case trimmedLine == "=======":
|
||||
return MIDDLE
|
||||
return TARGET
|
||||
case strings.HasPrefix(trimmedLine, ">>>>>>> "):
|
||||
return END
|
||||
default:
|
||||
|
||||
@@ -43,12 +43,16 @@ func TestDetermineLineType(t *testing.T) {
|
||||
},
|
||||
{
|
||||
line: "=======",
|
||||
expected: MIDDLE,
|
||||
expected: TARGET,
|
||||
},
|
||||
{
|
||||
line: ">>>>>>> blah",
|
||||
expected: END,
|
||||
},
|
||||
{
|
||||
line: "||||||| adf33b9",
|
||||
expected: ANCESTOR,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
||||
78
pkg/gui/mergeconflicts/merge_conflict.go
Normal file
78
pkg/gui/mergeconflicts/merge_conflict.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package mergeconflicts
|
||||
|
||||
// mergeConflict : A git conflict with a start, ancestor (if exists), target, and end corresponding to line
|
||||
// numbers in the file where the conflict markers appear.
|
||||
// If no ancestor is present (i.e. we're not using the diff3 algorithm), then
|
||||
// the `ancestor` field's value will be -1
|
||||
type mergeConflict struct {
|
||||
start int
|
||||
ancestor int
|
||||
target int
|
||||
end int
|
||||
}
|
||||
|
||||
func (c *mergeConflict) hasAncestor() bool {
|
||||
return c.ancestor >= 0
|
||||
}
|
||||
|
||||
func (c *mergeConflict) isMarkerLine(i int) bool {
|
||||
return i == c.start ||
|
||||
i == c.ancestor ||
|
||||
i == c.target ||
|
||||
i == c.end
|
||||
}
|
||||
|
||||
type Selection int
|
||||
|
||||
const (
|
||||
TOP Selection = iota
|
||||
MIDDLE
|
||||
BOTTOM
|
||||
ALL
|
||||
)
|
||||
|
||||
func (s Selection) isIndexToKeep(conflict *mergeConflict, i int) bool {
|
||||
// we're only handling one conflict at a time so any lines outside this
|
||||
// conflict we'll keep
|
||||
if i < conflict.start || conflict.end < i {
|
||||
return true
|
||||
}
|
||||
|
||||
if conflict.isMarkerLine(i) {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.selected(conflict, i)
|
||||
}
|
||||
|
||||
func (s Selection) bounds(c *mergeConflict) (int, int) {
|
||||
switch s {
|
||||
case TOP:
|
||||
if c.hasAncestor() {
|
||||
return c.start, c.ancestor
|
||||
} else {
|
||||
return c.start, c.target
|
||||
}
|
||||
case MIDDLE:
|
||||
return c.ancestor, c.target
|
||||
case BOTTOM:
|
||||
return c.target, c.end
|
||||
case ALL:
|
||||
return c.start, c.end
|
||||
}
|
||||
|
||||
panic("unexpected selection for merge conflict")
|
||||
}
|
||||
|
||||
func (s Selection) selected(c *mergeConflict, idx int) bool {
|
||||
start, end := s.bounds(c)
|
||||
return start < idx && idx < end
|
||||
}
|
||||
|
||||
func availableSelections(c *mergeConflict) []Selection {
|
||||
if c.hasAncestor() {
|
||||
return []Selection{TOP, MIDDLE, BOTTOM}
|
||||
} else {
|
||||
return []Selection{TOP, BOTTOM}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,11 @@ func ColoredConflictFile(content string, state *State, hasFocus bool) string {
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
textStyle := theme.DefaultTextColor
|
||||
if i == conflict.start || i == conflict.middle || i == conflict.end {
|
||||
if conflict.isMarkerLine(i) {
|
||||
textStyle = style.FgRed
|
||||
}
|
||||
|
||||
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) {
|
||||
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.Selection()) {
|
||||
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor).SetBold()
|
||||
}
|
||||
if i == conflict.end && len(remainingConflicts) > 0 {
|
||||
@@ -35,6 +35,7 @@ func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool {
|
||||
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
||||
func shouldHighlightLine(index int, conflict *mergeConflict, selection Selection) bool {
|
||||
selectionStart, selectionEnd := selection.bounds(conflict)
|
||||
return index >= selectionStart && index <= selectionEnd
|
||||
}
|
||||
|
||||
@@ -7,58 +7,60 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type Selection int
|
||||
|
||||
const (
|
||||
TOP Selection = iota
|
||||
BOTTOM
|
||||
BOTH
|
||||
)
|
||||
|
||||
// mergeConflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict markers appear
|
||||
type mergeConflict struct {
|
||||
start int
|
||||
middle int
|
||||
end int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
sync.Mutex
|
||||
|
||||
conflicts []*mergeConflict
|
||||
// this is the index of the above `conflicts` field which is currently selected
|
||||
conflictIndex int
|
||||
conflictTop bool
|
||||
conflicts []*mergeConflict
|
||||
EditHistory *stack.Stack
|
||||
|
||||
// this is the index of the selected conflict's available selections slice e.g. [TOP, MIDDLE, BOTTOM]
|
||||
// We use this to know which hunk of the conflict is selected.
|
||||
selectionIndex int
|
||||
|
||||
// this allows us to undo actions
|
||||
EditHistory *stack.Stack
|
||||
}
|
||||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
Mutex: sync.Mutex{},
|
||||
conflictIndex: 0,
|
||||
conflictTop: true,
|
||||
conflicts: []*mergeConflict{},
|
||||
EditHistory: stack.New(),
|
||||
Mutex: sync.Mutex{},
|
||||
conflictIndex: 0,
|
||||
selectionIndex: 0,
|
||||
conflicts: []*mergeConflict{},
|
||||
EditHistory: stack.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectTopOption() {
|
||||
s.conflictTop = true
|
||||
func (s *State) setConflictIndex(index int) {
|
||||
if len(s.conflicts) == 0 {
|
||||
s.conflictIndex = 0
|
||||
} else {
|
||||
s.conflictIndex = clamp(index, 0, len(s.conflicts)-1)
|
||||
}
|
||||
s.setSelectionIndex(s.selectionIndex)
|
||||
}
|
||||
|
||||
func (s *State) SelectBottomOption() {
|
||||
s.conflictTop = false
|
||||
func (s *State) setSelectionIndex(index int) {
|
||||
if selections := s.availableSelections(); len(selections) != 0 {
|
||||
s.selectionIndex = clamp(index, 0, len(selections)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectNextConflictHunk() {
|
||||
s.setSelectionIndex(s.selectionIndex + 1)
|
||||
}
|
||||
|
||||
func (s *State) SelectPrevConflictHunk() {
|
||||
s.setSelectionIndex(s.selectionIndex - 1)
|
||||
}
|
||||
|
||||
func (s *State) SelectNextConflict() {
|
||||
if s.conflictIndex < len(s.conflicts)-1 {
|
||||
s.conflictIndex++
|
||||
}
|
||||
s.setConflictIndex(s.conflictIndex + 1)
|
||||
}
|
||||
|
||||
func (s *State) SelectPrevConflict() {
|
||||
if s.conflictIndex > 0 {
|
||||
s.conflictIndex--
|
||||
}
|
||||
s.setConflictIndex(s.conflictIndex - 1)
|
||||
}
|
||||
|
||||
func (s *State) PushFileSnapshot(content string) {
|
||||
@@ -87,12 +89,7 @@ func (s *State) SetConflictsFromCat(cat string) {
|
||||
|
||||
func (s *State) setConflicts(conflicts []*mergeConflict) {
|
||||
s.conflicts = conflicts
|
||||
|
||||
if s.conflictIndex > len(s.conflicts)-1 {
|
||||
s.conflictIndex = len(s.conflicts) - 1
|
||||
} else if s.conflictIndex < 0 {
|
||||
s.conflictIndex = 0
|
||||
}
|
||||
s.setConflictIndex(s.conflictIndex)
|
||||
}
|
||||
|
||||
func (s *State) NoConflicts() bool {
|
||||
@@ -100,11 +97,17 @@ func (s *State) NoConflicts() bool {
|
||||
}
|
||||
|
||||
func (s *State) Selection() Selection {
|
||||
if s.conflictTop {
|
||||
return TOP
|
||||
} else {
|
||||
return BOTTOM
|
||||
if selections := s.availableSelections(); len(selections) > 0 {
|
||||
return selections[s.selectionIndex]
|
||||
}
|
||||
return TOP
|
||||
}
|
||||
|
||||
func (s *State) availableSelections() []Selection {
|
||||
if conflict := s.currentConflict(); conflict != nil {
|
||||
return availableSelections(conflict)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) IsFinalConflict() bool {
|
||||
@@ -116,10 +119,13 @@ func (s *State) Reset() {
|
||||
}
|
||||
|
||||
func (s *State) GetConflictMiddle() int {
|
||||
return s.currentConflict().middle
|
||||
return s.currentConflict().target
|
||||
}
|
||||
|
||||
func (s *State) ContentAfterConflictResolve(path string, selection Selection) (bool, string, error) {
|
||||
func (s *State) ContentAfterConflictResolve(
|
||||
path string,
|
||||
selection Selection,
|
||||
) (bool, string, error) {
|
||||
conflict := s.currentConflict()
|
||||
if conflict == nil {
|
||||
return false, "", nil
|
||||
@@ -127,7 +133,7 @@ func (s *State) ContentAfterConflictResolve(path string, selection Selection) (b
|
||||
|
||||
content := ""
|
||||
err := utils.ForEachLineInFile(path, func(line string, i int) {
|
||||
if !isIndexToDelete(i, conflict, selection) {
|
||||
if selection.isIndexToKeep(conflict, i) {
|
||||
content += line
|
||||
}
|
||||
})
|
||||
@@ -139,15 +145,11 @@ func (s *State) ContentAfterConflictResolve(path string, selection Selection) (b
|
||||
return true, content, nil
|
||||
}
|
||||
|
||||
func isIndexToDelete(i int, conflict *mergeConflict, selection Selection) bool {
|
||||
isMarkerLine :=
|
||||
i == conflict.middle ||
|
||||
i == conflict.start ||
|
||||
i == conflict.end
|
||||
|
||||
isUnwantedContent :=
|
||||
(selection == BOTTOM && conflict.start < i && i < conflict.middle) ||
|
||||
(selection == TOP && conflict.middle < i && i < conflict.end)
|
||||
|
||||
return isMarkerLine || isUnwantedContent
|
||||
func clamp(x int, min int, max int) int {
|
||||
if x < min {
|
||||
return min
|
||||
} else if x > max {
|
||||
return max
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
@@ -58,37 +58,57 @@ bar
|
||||
=======
|
||||
baz
|
||||
>>>>>>> branch
|
||||
|
||||
<<<<<<< HEAD
|
||||
foo
|
||||
||||||| fffffff
|
||||
bar
|
||||
=======
|
||||
baz
|
||||
>>>>>>> branch
|
||||
`,
|
||||
expected: []*mergeConflict{
|
||||
{
|
||||
start: 0,
|
||||
middle: 2,
|
||||
end: 4,
|
||||
start: 0,
|
||||
ancestor: -1,
|
||||
target: 2,
|
||||
end: 4,
|
||||
},
|
||||
{
|
||||
start: 6,
|
||||
middle: 9,
|
||||
end: 11,
|
||||
start: 6,
|
||||
ancestor: -1,
|
||||
target: 9,
|
||||
end: 11,
|
||||
},
|
||||
{
|
||||
start: 13,
|
||||
middle: 15,
|
||||
end: 17,
|
||||
start: 13,
|
||||
ancestor: -1,
|
||||
target: 15,
|
||||
end: 17,
|
||||
},
|
||||
{
|
||||
start: 19,
|
||||
middle: 21,
|
||||
end: 23,
|
||||
start: 19,
|
||||
ancestor: -1,
|
||||
target: 21,
|
||||
end: 23,
|
||||
},
|
||||
{
|
||||
start: 25,
|
||||
middle: 27,
|
||||
end: 29,
|
||||
start: 25,
|
||||
ancestor: -1,
|
||||
target: 27,
|
||||
end: 29,
|
||||
},
|
||||
{
|
||||
start: 31,
|
||||
middle: 34,
|
||||
end: 36,
|
||||
start: 31,
|
||||
ancestor: -1,
|
||||
target: 34,
|
||||
end: 36,
|
||||
},
|
||||
{
|
||||
start: 38,
|
||||
ancestor: 40,
|
||||
target: 42,
|
||||
end: 44,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
@@ -58,5 +59,19 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
||||
},
|
||||
reset: gui.exitCherryPickingMode,
|
||||
},
|
||||
{
|
||||
isActive: func() bool {
|
||||
return gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL
|
||||
},
|
||||
description: func() string {
|
||||
workingTreeState := gui.GitCommand.WorkingTreeState()
|
||||
return style.FgYellow.Sprintf(
|
||||
"%s %s",
|
||||
workingTreeState,
|
||||
style.AttrUnderline.Sprint(gui.Tr.ResetInParentheses),
|
||||
)
|
||||
},
|
||||
reset: gui.abortMergeOrRebaseWithConfirm,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,16 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||
|
||||
func (gui *Gui) displayDescription(binding *Binding) string {
|
||||
if binding.OpensMenu {
|
||||
return style.FgMagenta.Sprintf("%s...", binding.Description)
|
||||
return opensMenuStyle(binding.Description)
|
||||
}
|
||||
|
||||
return style.FgCyan.Sprint(binding.Description)
|
||||
}
|
||||
|
||||
func opensMenuStyle(str string) string {
|
||||
return style.FgMagenta.Sprintf("%s...", str)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateOptionsMenu() error {
|
||||
view := gui.g.CurrentView()
|
||||
if view == nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user