Compare commits
216 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd1d1996df | ||
|
|
963fcc1444 | ||
|
|
c6825e3d0d | ||
|
|
20bdba15f6 | ||
|
|
e636857057 | ||
|
|
1ae8523098 | ||
|
|
8eb802d3a0 | ||
|
|
6fc031c523 | ||
|
|
8c93289a72 | ||
|
|
b1df0fafa2 | ||
|
|
15046a0454 | ||
|
|
fb9b6314a0 | ||
|
|
0719a3e36e | ||
|
|
a3b0efb82e | ||
|
|
bde324820d | ||
|
|
bbdbbd0b1b | ||
|
|
d4f3b292e6 | ||
|
|
39eb937830 | ||
|
|
fbab5bd444 | ||
|
|
12ca922a41 | ||
|
|
f4e552f982 | ||
|
|
94d26d00ba | ||
|
|
d80d1f8493 | ||
|
|
ace4350319 | ||
|
|
4441cf1045 | ||
|
|
cf99b47ec0 | ||
|
|
546eb50bac | ||
|
|
5e094c8a7c | ||
|
|
c683f2c96c | ||
|
|
e5a372fa2d | ||
|
|
02f45b679f | ||
|
|
b1cda65dcf | ||
|
|
74ce65d9ff | ||
|
|
ccebe5e069 | ||
|
|
b6ec667de0 | ||
|
|
390b7ddc5e | ||
|
|
38739b16bc | ||
|
|
27525f1d42 | ||
|
|
43a9dc48e0 | ||
|
|
440eb387d7 | ||
|
|
28ffaf9348 | ||
|
|
d7da6dde0e | ||
|
|
e000620cdf | ||
|
|
f09309485a | ||
|
|
e04e2ebab5 | ||
|
|
91a107eb6f | ||
|
|
5ce9e0193a | ||
|
|
4c71c26593 | ||
|
|
abdd2455bb | ||
|
|
c33f8d2790 | ||
|
|
8e9d08bc10 | ||
|
|
9593129e6a | ||
|
|
267da3b4db | ||
|
|
c9ded489c9 | ||
|
|
4c73d070ac | ||
|
|
121b9d0715 | ||
|
|
fbb33b7abc | ||
|
|
7178bab6b4 | ||
|
|
2d7452bfaa | ||
|
|
b0f3bfef27 | ||
|
|
7bc6dc5cf3 | ||
|
|
ee7b634dce | ||
|
|
b0bd752180 | ||
|
|
4d14af5d4b | ||
|
|
7953e58c74 | ||
|
|
549d73a0b1 | ||
|
|
8301bba8ad | ||
|
|
78f17aa541 | ||
|
|
7578a7466f | ||
|
|
8681a6b4e2 | ||
|
|
efed313721 | ||
|
|
795cf39ddf | ||
|
|
f08f248cb7 | ||
|
|
3c20425649 | ||
|
|
dfc689411b | ||
|
|
2295407a45 | ||
|
|
828a2acd26 | ||
|
|
843b8ceab0 | ||
|
|
011451464f | ||
|
|
32d170621c | ||
|
|
464d022a86 | ||
|
|
6a0066253f | ||
|
|
d627b3bfc8 | ||
|
|
952c62df37 | ||
|
|
b6cc1c9492 | ||
|
|
39ae122304 | ||
|
|
c34c6926d5 | ||
|
|
4fe512ff3a | ||
|
|
4197921465 | ||
|
|
4b69ab08c1 | ||
|
|
f3a0058eb9 | ||
|
|
633b6f596d | ||
|
|
e6274c0757 | ||
|
|
0898a7bb57 | ||
|
|
fafd5234bd | ||
|
|
8cb10f76e4 | ||
|
|
f1d7f59e49 | ||
|
|
bc9a99387f | ||
|
|
5289d49f75 | ||
|
|
69e9f6d29d | ||
|
|
0b42437052 | ||
|
|
ae0f750770 | ||
|
|
9fe7e0d63d | ||
|
|
8935794e28 | ||
|
|
d44ff447bd | ||
|
|
798d3e2d54 | ||
|
|
e8f99c3326 | ||
|
|
1a5f380c00 | ||
|
|
b4827a98ca | ||
|
|
3ea5e4d4b2 | ||
|
|
5f77ac8d6f | ||
|
|
5d0cf3d919 | ||
|
|
4b1da0cf3c | ||
|
|
862ced3bd0 | ||
|
|
79b256a0fa | ||
|
|
0d6ff7d1b7 | ||
|
|
ecc5fe24a9 | ||
|
|
1fb2317bac | ||
|
|
6246eb9717 | ||
|
|
8f763c42b6 | ||
|
|
6472bda29e | ||
|
|
c0cad91cb6 | ||
|
|
1149dea4b2 | ||
|
|
6a6024e38f | ||
|
|
8901d11674 | ||
|
|
8b7f7cbc30 | ||
|
|
b6d0bdfa2d | ||
|
|
44896bcd51 | ||
|
|
bdf2b2d5c4 | ||
|
|
035726f650 | ||
|
|
1abb3cd566 | ||
|
|
f7772f00c4 | ||
|
|
216b5341ae | ||
|
|
eeeef9ca86 | ||
|
|
cc9293b386 | ||
|
|
efe43077bc | ||
|
|
949c7726d1 | ||
|
|
0b7bda291c | ||
|
|
872cf0d726 | ||
|
|
af09223dd5 | ||
|
|
7d62f103e4 | ||
|
|
9e85d37fb9 | ||
|
|
8dee06f83a | ||
|
|
82fe4aa6c0 | ||
|
|
50c169e0a3 | ||
|
|
7364525bf5 | ||
|
|
54910fdb76 | ||
|
|
332a3c4cbf | ||
|
|
ac41c41809 | ||
|
|
96a9df04ed | ||
|
|
b7cc4158d5 | ||
|
|
2bbe6269cd | ||
|
|
eb54189683 | ||
|
|
e8e59306fc | ||
|
|
8af3fe3b4a | ||
|
|
3103247e8f | ||
|
|
1629a7d280 | ||
|
|
b5a5169372 | ||
|
|
4b4bfae4f4 | ||
|
|
d5639e6e95 | ||
|
|
9e67f74ca3 | ||
|
|
e3ddfbf2b8 | ||
|
|
1ea78c7ae7 | ||
|
|
e7af3bf55d | ||
|
|
e52cec9cdf | ||
|
|
5bb48b51a0 | ||
|
|
d2e1b35eee | ||
|
|
ef204b0adf | ||
|
|
f742434043 | ||
|
|
d3b34ce323 | ||
|
|
89c2f4f2ff | ||
|
|
5a0f23e6d6 | ||
|
|
5e05e8b62b | ||
|
|
1f7273af23 | ||
|
|
2b8302bced | ||
|
|
1b94462410 | ||
|
|
120bb443fe | ||
|
|
6fc3c03c4b | ||
|
|
46b79c7c61 | ||
|
|
4782d8aa1f | ||
|
|
fe4e305410 | ||
|
|
040c1fc302 | ||
|
|
5edea5a8dc | ||
|
|
d2b65537f6 | ||
|
|
1183f68e19 | ||
|
|
da6fe01eca | ||
|
|
c27cea6f30 | ||
|
|
cd0532b4d6 | ||
|
|
c9de6c003b | ||
|
|
418621a9ff | ||
|
|
f871724ae6 | ||
|
|
def68ddc8f | ||
|
|
a31db3df9c | ||
|
|
64217a8a5b | ||
|
|
79079b54ea | ||
|
|
77a7619690 | ||
|
|
9f2d7adb8e | ||
|
|
07dd9c6bc8 | ||
|
|
45939171ea | ||
|
|
049849264e | ||
|
|
7e0d48c2a1 | ||
|
|
ad1468f66f | ||
|
|
058bcddc53 | ||
|
|
8288de0c84 | ||
|
|
1da2afd450 | ||
|
|
03de51747e | ||
|
|
3d698cd7c1 | ||
|
|
a48cc245e7 | ||
|
|
9ed3a8ee05 | ||
|
|
64daf1310d | ||
|
|
e5ba0d9d9c | ||
|
|
50e4e9d58d | ||
|
|
03b9db5e0a | ||
|
|
043cb2ea44 | ||
|
|
a62d70fbd5 | ||
|
|
053e80a08e |
@@ -279,12 +279,10 @@ Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to vie
|
||||
|
||||
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
|
||||
|
||||
## Work in progress
|
||||
## FAQ
|
||||
|
||||
This is still a work in progress so there's still bugs to iron out and as this
|
||||
is my first project in Go the code could no doubt use an increase in quality,
|
||||
but I'll be improving on it whenever I find the time. If you have any feedback
|
||||
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
|
||||
### I'm struggling to see the selected line
|
||||
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
|
||||
|
||||
## Social
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
* Linux: `~/.config/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
@@ -34,6 +34,7 @@ Default path for the config file:
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
@@ -60,7 +61,7 @@ Default path for the config file:
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: true
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
@@ -133,6 +134,7 @@ Default path for the config file:
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
@@ -248,7 +250,7 @@ If you have issues with a light terminal theme where you can't read / see the te
|
||||
|
||||
## Struggling to see selected line
|
||||
|
||||
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
|
||||
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
@@ -259,6 +261,24 @@ If you struggle to see the selected line I recomment using the reverse attribute
|
||||
- reverse
|
||||
```
|
||||
|
||||
The following has also worked for a couple of people:
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
- blue
|
||||
```
|
||||
|
||||
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
|
||||
|
||||
If you're still having trouble please raise an issue.
|
||||
|
||||
## Example Coloring
|
||||
|
||||

|
||||
|
||||
@@ -21,7 +21,7 @@ the `colorArg` key is for whether you want the `--color=always` arg in your `git
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: delta --dark --paging=never --24-bit-color=never
|
||||
pager: delta --dark --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
@@ -109,7 +109,8 @@
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits)
|
||||
@@ -170,10 +171,11 @@
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
@@ -205,8 +207,8 @@
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
||||
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
@@ -174,6 +175,7 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
@@ -205,8 +207,8 @@
|
||||
## Hooft Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>¦</kbd>: scroll omhoog (fn+down)
|
||||
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Patch Bouwen)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
@@ -109,7 +109,8 @@
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commity)
|
||||
@@ -174,6 +175,7 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
@@ -205,8 +207,8 @@
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
13
go.mod
13
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.2.0 // indirect
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
@@ -19,16 +20,16 @@ require (
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
|
||||
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/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.10
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 // indirect
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
@@ -38,7 +39,9 @@ require (
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
66
go.sum
66
go.sum
@@ -31,8 +31,17 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
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 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
|
||||
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.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.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
@@ -69,8 +78,34 @@ github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOe
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
@@ -94,6 +129,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
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 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -105,18 +144,19 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
|
||||
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 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 h1:3vzIuru1svOK2sXlg4XcrO3KkGRneIejmfQfR+ptSW8=
|
||||
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -168,17 +208,35 @@ 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 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
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-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
||||
@@ -97,6 +97,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
@@ -182,7 +183,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
}
|
||||
|
||||
// if we are not in a git repo, we ask if we want to `git init`
|
||||
if err := app.OSCommand.RunCommand("git status"); err != nil {
|
||||
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -225,6 +226,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -237,7 +239,7 @@ func (app *App) Run() error {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := app.Gui.RunWithSubprocesses()
|
||||
err := app.Gui.RunAndHandleError()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
|
||||
return c.RunCommand("git checkout -b %s %s", name, base)
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
// the first returned string is the name and the second is the displayname
|
||||
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
||||
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
}
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
|
||||
output, err := c.RunCommandWithOutput("git branch --contains")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
output, err := c.OSCommand.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}", branchName)
|
||||
return strings.TrimSpace(output), err
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@ func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.OSCommand.RunCommand("git branch -u %s", upstream)
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -134,24 +134,24 @@ func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.OSCommand.RunCommand("git merge --abort")
|
||||
return c.RunCommand("git merge --abort")
|
||||
}
|
||||
|
||||
func (c *GitCommand) IsHeadDetached() bool {
|
||||
err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD")
|
||||
err := c.RunCommand("git symbolic-ref -q HEAD")
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --hard " + ref)
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + ref)
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -12,7 +11,7 @@ import (
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))
|
||||
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
@@ -25,7 +24,7 @@ func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
|
||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
||||
@@ -75,7 +74,7 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (c *GitCommand) Revert(sha string) error {
|
||||
return c.OSCommand.RunCommand("git revert %s", sha)
|
||||
return c.RunCommand("git revert %s", sha)
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
@@ -95,5 +94,5 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
|
||||
return c.RunCommand("git commit --fixup=%s", sha)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
|
||||
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -2,48 +2,47 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.OSCommand.RunCommand("git add -A")
|
||||
return c.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
// UnstageAll stages all files
|
||||
// UnstageAll unstages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.OSCommand.RunCommand("git reset")
|
||||
return c.RunCommand("git reset")
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
command := "git rm --cached --force %s"
|
||||
if tracked {
|
||||
command = "git reset HEAD %s"
|
||||
// we accept an array of filenames for the cases where a file has been renamed i.e.
|
||||
// we accept the current name and the previous name
|
||||
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
|
||||
command := "git rm --cached --force -- %s"
|
||||
if reset {
|
||||
command = "git reset HEAD -- %s"
|
||||
}
|
||||
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
for _, name := range fileNames {
|
||||
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
||||
return err
|
||||
@@ -58,20 +57,19 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
|
||||
return nil, nil, errors.New("Expected renamed file")
|
||||
}
|
||||
|
||||
// we've got a file that represents a rename from one file to another. Unfortunately
|
||||
// our File abstraction fails to consider this case, so here we will refetch
|
||||
// we've got a file that represents a rename from one file to another. Here we will refetch
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file. At some point we should fix the abstraction itself
|
||||
// again for the before file and after file.
|
||||
|
||||
split := strings.Split(file.Name, " -> ")
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
var beforeFile *models.File
|
||||
var afterFile *models.File
|
||||
for _, f := range filesWithoutRenames {
|
||||
if f.Name == split[0] {
|
||||
if f.Name == file.PreviousName {
|
||||
beforeFile = f
|
||||
}
|
||||
if f.Name == split[1] {
|
||||
|
||||
if f.Name == file.Name {
|
||||
afterFile = f
|
||||
}
|
||||
}
|
||||
@@ -107,24 +105,76 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
|
||||
if file.ShortStatus == "AA" {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !file.Tracked {
|
||||
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.Added {
|
||||
return c.removeFile(file.Name)
|
||||
}
|
||||
return c.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
|
||||
// this could be more efficient but we would need to handle all the edge cases
|
||||
return node.ForEachFile(c.DiscardAllFileChanges)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
|
||||
if err := c.RemoveUntrackedDirFiles(node); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
|
||||
untrackedFilePaths := node.GetPathsMatching(
|
||||
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
|
||||
)
|
||||
|
||||
for _, path := range untrackedFilePaths {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
|
||||
return c.RunCommand("git checkout -- %s", quotedFileName)
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
@@ -139,23 +189,22 @@ func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
|
||||
fileName := c.OSCommand.Quote(split[len(split)-1])
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges && !cached {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
@@ -170,7 +219,7 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
@@ -196,7 +245,7 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
||||
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
return c.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
@@ -206,7 +255,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.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -232,17 +281,17 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.OSCommand.RunCommand("git checkout -- .")
|
||||
return c.RunCommand("git checkout -- .")
|
||||
}
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
|
||||
return c.RunCommand("git rm -r --cached %s", name)
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
return c.RunCommand("git clean -fd")
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
@@ -266,10 +315,13 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
// falling back to core.editor, GIT_EDITOR, VISUAL, EDITOR, then vi
|
||||
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("VISUAL")
|
||||
}
|
||||
@@ -282,7 +334,7 @@ func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
return nil, errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
@@ -54,10 +55,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
|
||||
if err := verifyInGitRepo(osCommand.RunCommand); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,10 +85,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
return gitCommand, nil
|
||||
}
|
||||
|
||||
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
|
||||
return runCmd("git status")
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
gitDir := env.GetGitDirEnv()
|
||||
if gitDir != "" {
|
||||
@@ -120,6 +113,18 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
|
||||
if err = chdir(".."); err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atRoot := currentPath == filepath.Dir(currentPath)
|
||||
if atRoot {
|
||||
// we should never really land here: the code that creates GitCommand should
|
||||
// verify we're in a git directory
|
||||
return errors.New("Must open lazygit in a git repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,3 +194,36 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
||||
return osCommand.RunCommand("git rev-parse --git-dir")
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
// TODO: have this retry logic in other places we run the command
|
||||
waitTime := 50 * time.Millisecond
|
||||
retryCount := 5
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
|
||||
if err != nil {
|
||||
// if we have an error based on the index lock, we should wait a bit and then retry
|
||||
if strings.Contains(output, ".git/index.lock") {
|
||||
c.Log.Error(output)
|
||||
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
||||
attempt++
|
||||
time.Sleep(waitTime)
|
||||
if attempt < retryCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,43 +61,6 @@ func (f fileInfoMock) Sys() interface{} {
|
||||
return f.sys
|
||||
}
|
||||
|
||||
// TestVerifyInGitRepo is a function.
|
||||
func TestVerifyInGitRepo(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runCmd func(string, ...interface{}) error
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Valid git repository",
|
||||
func(string, ...interface{}) error {
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Not a valid git repository",
|
||||
func(string, ...interface{}) error {
|
||||
return fmt.Errorf("fatal: Not a git repository (or any of the parent directories): .git")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, `fatal: .ot a git repository \(or any of the parent directories\s?\/?\): \.git`, err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(verifyInGitRepo(s.runCmd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNavigateToRepoRootDirectory is a function.
|
||||
func TestNavigateToRepoRootDirectory(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -233,7 +196,7 @@ func TestNewGitCommand(t *testing.T) {
|
||||
},
|
||||
func(gitCmd *GitCommand, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, `fatal: .ot a git repository ((\(or any of the parent directories\): \.git)|(\(or any parent up to mount point \/\)))`, err.Error())
|
||||
assert.Regexp(t, `Must open lazygit in a git repository`, err.Error())
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -344,6 +307,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
@@ -356,6 +320,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
@@ -368,6 +333,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
@@ -380,6 +346,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
@@ -392,6 +359,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
@@ -456,87 +424,6 @@ func TestGitCommandCommitAmend(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandMergeStatusFiles is a function.
|
||||
func TestGitCommandMergeStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
oldFiles []*models.File
|
||||
newFiles []*models.File
|
||||
test func([]*models.File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Old file and new file are the same",
|
||||
[]*models.File{},
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "new_file.txt",
|
||||
},
|
||||
},
|
||||
func(files []*models.File) {
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "new_file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, files, 1)
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files to merge, with some identical",
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file2.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file3.txt",
|
||||
},
|
||||
},
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "new_file4.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file5.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
},
|
||||
func(files []*models.File) {
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file4.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file5.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, files, 3)
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetCommitDifferences is a function.
|
||||
func TestGitCommandGetCommitDifferences(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -600,7 +487,7 @@ func TestGitCommandRenameCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
|
||||
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
@@ -1037,7 +924,7 @@ func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"add", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
@@ -1051,7 +938,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
tracked bool
|
||||
reset bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -1059,7 +946,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
"Remove an untracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"rm", "--cached", "--force", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
@@ -1072,7 +959,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
"Remove a tracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "HEAD", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
@@ -1087,12 +974,15 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.UnStageFile("test.txt", s.tracked))
|
||||
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
// these tests don't cover everything, in part because we already have an integration
|
||||
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
|
||||
// when the 'what' is what matters
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
@@ -1146,6 +1036,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
},
|
||||
func(string) error {
|
||||
return fmt.Errorf("an error occurred when removing file")
|
||||
@@ -1277,6 +1168,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(filename string) error {
|
||||
@@ -1301,6 +1193,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(filename string) error {
|
||||
@@ -1455,7 +1348,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"File not tracked and file has no staged changes",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
@@ -2120,7 +2013,7 @@ func TestEditFile(t *testing.T) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,45 +4,37 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*models.CommitFile, error) {
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
|
||||
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
|
||||
return c.getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
changeStatus := line[0:1]
|
||||
name := line[2:]
|
||||
status := patch.UNSELECTED
|
||||
if patchManager != nil && patchManager.To == parent {
|
||||
status = patchManager.GetFileStatus(name)
|
||||
}
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Parent: parent,
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
PatchStatus: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
// GetStatusFiles git status files
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
@@ -37,26 +39,35 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
name := statusString[3:]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
previousName := ""
|
||||
if strings.Contains(name, RENAME_SEPARATOR) {
|
||||
split := strings.Split(name, RENAME_SEPARATOR)
|
||||
name = split[1]
|
||||
previousName = split[0]
|
||||
}
|
||||
|
||||
file := &models.File{
|
||||
Name: filename,
|
||||
Name: name,
|
||||
PreviousName: previousName,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(filename),
|
||||
Type: c.OSCommand.FileType(name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
@@ -72,40 +83,22 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
appendedIndexes := []int{}
|
||||
|
||||
// retain position of files we already could see
|
||||
result := []*models.File{}
|
||||
for _, oldFile := range oldFiles {
|
||||
for newIndex, newFile := range newFiles {
|
||||
if utils.IncludesInt(appendedIndexes, newIndex) {
|
||||
continue
|
||||
}
|
||||
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
|
||||
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
|
||||
|
||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
}
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
for i := 0; i < len(splitLines)-1; i++ {
|
||||
original := splitLines[i]
|
||||
if strings.HasPrefix(original, "R ") {
|
||||
next := splitLines[i+1]
|
||||
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
|
||||
splitLines[i] = updated
|
||||
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// append any new files to the end
|
||||
for index, newFile := range newFiles {
|
||||
if !utils.IncludesInt(appendedIndexes, index) {
|
||||
result = append(result, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return strings.Join(splitLines, "\n"), nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := c.OSCommand.RunCommandWithOutput("git stash list --name-only")
|
||||
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,8 @@ package models
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
|
||||
Parent string
|
||||
Name string
|
||||
|
||||
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
|
||||
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
|
||||
// TODO: rename this to Path
|
||||
Name string
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -10,9 +8,11 @@ import (
|
||||
// duplicating this for now
|
||||
type File struct {
|
||||
Name string
|
||||
PreviousName string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Added bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
@@ -21,15 +21,27 @@ type File struct {
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
// sometimes we need to deal with either a node (which contains a file) or an actual file
|
||||
type IFile interface {
|
||||
GetHasUnstagedChanges() bool
|
||||
GetHasStagedChanges() bool
|
||||
GetIsTracked() bool
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
return strings.Contains(f.Name, RENAME_SEPARATOR)
|
||||
return f.PreviousName != ""
|
||||
}
|
||||
|
||||
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
|
||||
func (f *File) Names() []string {
|
||||
return strings.Split(f.Name, RENAME_SEPARATOR)
|
||||
result := []string{f.Name}
|
||||
if f.PreviousName != "" {
|
||||
result = append(result, f.PreviousName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
@@ -58,3 +70,20 @@ func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) GetHasUnstagedChanges() bool {
|
||||
return f.HasUnstagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetHasStagedChanges() bool {
|
||||
return f.HasStagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetIsTracked() bool {
|
||||
return f.Tracked
|
||||
}
|
||||
|
||||
func (f *File) GetPath() string {
|
||||
// TODO: remove concept of name; just use path
|
||||
return f.Name
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -24,14 +23,13 @@ import (
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
FallbackEscapedQuote string
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
@@ -73,7 +71,10 @@ type RunCommandOptions struct {
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
|
||||
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
@@ -97,7 +98,19 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
cmd := c.ExecutableFromString(command)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", command).Error(err)
|
||||
c.Log.WithField("command", command).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -129,7 +142,7 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = strconv.Quote(commandStr)
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
@@ -174,6 +187,17 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.Log.WithField("command", command).Info("RunShellCommand")
|
||||
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
@@ -186,16 +210,6 @@ func (c *OSCommand) FileType(path string) string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
// RunDirectCommand wrapper around direct commands
|
||||
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||
|
||||
return sanitisedCommandOutput(
|
||||
c.Command(c.Platform.Shell, c.Platform.ShellArg, command).
|
||||
CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
@@ -243,20 +257,24 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
if strings.Contains(message, c.Platform.EscapedQuote) {
|
||||
escapedQuote = c.Platform.FallbackEscapedQuote
|
||||
}
|
||||
return escapedQuote + message + escapedQuote
|
||||
// PrepareShellSubProcess returns the pointer to a custom command
|
||||
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
@@ -352,11 +370,6 @@ func (c *OSCommand) GetLazygitPath() string {
|
||||
return `"` + filepath.ToSlash(ex) + `"`
|
||||
}
|
||||
|
||||
// RunCustomCommand returns the pointer to a custom command
|
||||
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
|
||||
|
||||
@@ -8,13 +8,12 @@ import (
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: "cat",
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: "'",
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
FallbackEscapedQuote: "\"",
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: []string{"cat"},
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.OS = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
@@ -129,7 +131,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.FallbackEscapedQuote + "hello 'test'" + osCommand.Platform.FallbackEscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -142,18 +144,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \"test\"" + osCommand.Platform.EscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandUnquote is a function.
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
expected := "hello test"
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: "cmd /c type",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
FallbackEscapedQuote: "\\'",
|
||||
OS: "windows",
|
||||
CatCmd: []string{"cmd", "/c", "type"},
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -9,9 +8,11 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchStatus int
|
||||
|
||||
const (
|
||||
// UNSELECTED is for when the commit file has not been added to the patch in any way
|
||||
UNSELECTED = iota
|
||||
UNSELECTED PatchStatus = iota
|
||||
// WHOLE is for when you want to add the whole diff of a file to the patch,
|
||||
// including e.g. if it was deleted
|
||||
WHOLE
|
||||
@@ -20,7 +21,7 @@ const (
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
mode int // one of WHOLE/PART
|
||||
mode PatchStatus
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
}
|
||||
@@ -81,20 +82,25 @@ func (p *PatchManager) removeFile(info *fileInfo) {
|
||||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ToggleFileWhole(filename string) error {
|
||||
func (p *PatchManager) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch info.mode {
|
||||
case UNSELECTED, PART:
|
||||
p.addFileWhole(info)
|
||||
case WHOLE:
|
||||
p.removeFile(info)
|
||||
default:
|
||||
return errors.New("unknown file mode")
|
||||
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -216,7 +222,11 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string) int {
|
||||
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
if parent != p.To {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if !ok {
|
||||
return UNSELECTED
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchLineKind int
|
||||
|
||||
const (
|
||||
PATCH_HEADER = iota
|
||||
PATCH_HEADER PatchLineKind = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
@@ -24,7 +26,7 @@ const (
|
||||
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
|
||||
|
||||
type PatchLine struct {
|
||||
Kind int
|
||||
Kind PatchLineKind
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
@@ -144,7 +146,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind int
|
||||
var lineKind PatchLineKind
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
|
||||
@@ -7,19 +7,19 @@ import (
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.OSCommand.RunCommand("git remote add %s %s", name, url)
|
||||
return c.RunCommand("git remote add %s %s", name, url)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.OSCommand.RunCommand("git remote remove %s", name)
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.OSCommand.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
||||
|
||||
@@ -4,13 +4,13 @@ import "fmt"
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
return c.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
@@ -22,7 +22,7 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
|
||||
if err := c.RunCommand("git stash --keep-index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
||||
if err := c.UnStageFile(file.Names(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,28 +69,28 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
|
||||
return c.OSCommand.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
// not doing an --init here because the user probably doesn't want that
|
||||
return c.OSCommand.RunCommand("git submodule update --force")
|
||||
return c.RunCommand("git submodule update --force")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if err := c.OSCommand.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
if err := c.OSCommand.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
c.Log.Error(err)
|
||||
}
|
||||
@@ -119,11 +119,11 @@ 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.OSCommand.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git submodule sync %s", path); err != nil {
|
||||
if err := c.RunCommand("git submodule sync %s", path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
||||
return c.OSCommand.RunCommand("git submodule init %s", path)
|
||||
return c.RunCommand("git submodule init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdate(path string) error {
|
||||
return c.OSCommand.RunCommand("git submodule update --init %s", path)
|
||||
return c.RunCommand("git submodule update --init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
|
||||
@@ -3,11 +3,11 @@ package commands
|
||||
import "fmt"
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
return c.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.OSCommand.RunCommand("git tag -d %s", tagName)
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
||||
|
||||
@@ -41,6 +41,7 @@ type AppConfigurer interface {
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
@@ -203,6 +204,16 @@ func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.UserConfig = userConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func configFilePath(filename string) (string, error) {
|
||||
folder, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,7 @@ type GuiConfig struct {
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
@@ -175,6 +176,7 @@ type KeybindingFilesConfig struct {
|
||||
ToggleStagedAll string `yaml:"toggleStagedAll"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
@@ -323,7 +325,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Reporting: "undetermined",
|
||||
SplashUpdatesIndex: 0,
|
||||
ConfirmOnQuit: false,
|
||||
QuitOnTopLevelReturn: true,
|
||||
QuitOnTopLevelReturn: false,
|
||||
Keybinding: KeybindingConfig{
|
||||
Universal: KeybindingUniversalConfig{
|
||||
Quit: "q",
|
||||
@@ -379,7 +381,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
DiffingMenuAlt: "<c-e>",
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<tab>",
|
||||
AppendNewline: "<a-enter>",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -398,6 +400,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
ToggleStagedAll: "a",
|
||||
ViewResetOptions: "D",
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
|
||||
@@ -97,10 +97,10 @@ func (gui *Gui) renderAppStatus() {
|
||||
for range ticker.C {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
if appStatus == "" {
|
||||
gui.renderString("appStatus", "")
|
||||
gui.renderString(gui.Views.AppStatus, "")
|
||||
return
|
||||
}
|
||||
gui.renderString("appStatus", appStatus)
|
||||
gui.renderString(gui.Views.AppStatus, appStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,9 +181,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
// the default behaviour when accordian 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()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
box := &boxlayout.Box{Window: "stash"}
|
||||
stashWindowAccessed := false
|
||||
for _, context := range gui.State.ContextStack {
|
||||
for _, context := range gui.State.ContextManager.ContextStack {
|
||||
if context.GetWindowName() == "stash" {
|
||||
stashWindowAccessed = true
|
||||
}
|
||||
@@ -278,9 +281,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
|
||||
func (gui *Gui) currentSideWindowName() string {
|
||||
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
|
||||
for idx := range gui.State.ContextStack {
|
||||
reversedIdx := len(gui.State.ContextStack) - 1 - idx
|
||||
context := gui.State.ContextStack[reversedIdx]
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
for idx := range gui.State.ContextManager.ContextStack {
|
||||
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
|
||||
context := gui.State.ContextManager.ContextStack[reversedIdx]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
return context.GetWindowName()
|
||||
|
||||
@@ -9,8 +9,10 @@ type Dimensions struct {
|
||||
Y1 int
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
ROW = iota
|
||||
ROW Direction = iota
|
||||
COLUMN
|
||||
)
|
||||
|
||||
@@ -26,10 +28,10 @@ const (
|
||||
|
||||
type Box struct {
|
||||
// Direction decides how the children boxes are laid out. ROW means the children will each form a row i.e. that they will be stacked on top of eachother.
|
||||
Direction int // ROW or COLUMN
|
||||
Direction Direction
|
||||
|
||||
// function which takes the width and height assigned to the box and decides which orientation it will have
|
||||
ConditionalDirection func(width int, height int) int
|
||||
ConditionalDirection func(width int, height int) Direction
|
||||
|
||||
Children []*Box
|
||||
|
||||
@@ -120,7 +122,7 @@ func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
||||
func (b *Box) getDirection(width int, height int) int {
|
||||
func (b *Box) getDirection(width int, height int) Direction {
|
||||
if b.ConditionalDirection != nil {
|
||||
return b.ConditionalDirection(width, height)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestArrangeWindows(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with narrow box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
&Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
@@ -111,7 +111,7 @@ func TestArrangeWindows(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with wide box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
&Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
@@ -31,13 +30,13 @@ func (gui *Gui) handleBranchSelect() error {
|
||||
var task updateTask
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -70,7 +69,7 @@ func (gui *Gui) refreshBranches() {
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -79,7 +78,7 @@ func (gui *Gui) refreshBranches() {
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleBranchPress() error {
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
@@ -90,7 +89,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreatePullRequestPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
@@ -101,7 +100,7 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
@@ -114,7 +113,7 @@ func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleGitFetch() error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -126,7 +125,7 @@ func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleForceCheckout() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
message := gui.Tr.SureForceCheckout
|
||||
title := gui.Tr.ForceCheckoutBranch
|
||||
@@ -208,7 +207,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckoutByName() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.BranchName + ":",
|
||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
||||
@@ -251,7 +250,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleDeleteBranch() error {
|
||||
return gui.deleteBranch(false)
|
||||
}
|
||||
|
||||
@@ -293,7 +292,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
}
|
||||
return gui.createErrorPanel(errMessage)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -328,7 +327,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMerge() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -337,7 +336,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRebaseOntoLocalBranch() error {
|
||||
selectedBranchName := gui.getSelectedBranch().Name
|
||||
return gui.handleRebaseOntoBranch(selectedBranchName)
|
||||
}
|
||||
@@ -369,7 +368,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFastForward() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
@@ -408,13 +407,13 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
} else {
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateResetToBranchMenu() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
@@ -423,15 +422,12 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
|
||||
return gui.createResetMenu(branch.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRenameBranch() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
|
||||
// way to get it to show up in the reflog)
|
||||
|
||||
promptForNewName := func() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
|
||||
@@ -440,13 +436,21 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
// need to checkout so that the branch shows up in our reflog and therefore
|
||||
// doesn't get lost among all the other branches when we switch to something else
|
||||
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
|
||||
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
|
||||
gui.refreshBranches()
|
||||
|
||||
// now that we've got our stuff again we need to find that branch and reselect it.
|
||||
for i, newBranch := range gui.State.Branches {
|
||||
if newBranch.Name == newBranchName {
|
||||
gui.State.Panels.Branches.SetSelectedLineIdx(i)
|
||||
if err := gui.State.Contexts.Branches.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -474,7 +478,7 @@ func (gui *Gui) currentBranch() *models.Branch {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
@@ -508,8 +512,8 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
context.GetPanelState().SetSelectedLineIdx(0)
|
||||
}
|
||||
|
||||
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
|
||||
if err := gui.pushContext(gui.Contexts.Branches.Context); err != nil {
|
||||
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
|
||||
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -550,5 +554,5 @@ func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
|
||||
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
||||
// git's branch naming requirement.
|
||||
func sanitizedBranchName(input string) string {
|
||||
return strings.ReplaceAll(input, " ", "-")
|
||||
return strings.Replace(input, " ", "-", -1)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (gui *Gui) handleCopyCommit() error {
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
|
||||
}
|
||||
|
||||
func (gui *Gui) commitsListForContext() []*models.Commit {
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (gui *Gui) handleCopyCommitRange() error {
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (gui *Gui) exitCherryPickingMode() error {
|
||||
return gui.rerenderContextViewIfPresent(contextKey)
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
|
||||
if contextKey == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -184,7 +184,7 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if view.Context == contextKey {
|
||||
if ContextKey(view.Context) == contextKey {
|
||||
if err := context.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
)
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
|
||||
func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
if selectedLine == -1 || selectedLine > len(gui.State.CommitFiles)-1 {
|
||||
if selectedLine == -1 || selectedLine > gui.State.CommitFileManager.GetItemsLength()-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.CommitFiles[selectedLine]
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFilePath() string {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFileSelect() error {
|
||||
gui.escapeLineByLinePanel()
|
||||
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := commitFile.Parent
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, commitFile.Name, false),
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -39,25 +56,25 @@ func (gui *Gui) handleCommitFileSelect() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleCheckoutCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
|
||||
if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleDiscardOldFileChange() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
|
||||
fileName := gui.getSelectedCommitFileName()
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardFileChangesTitle,
|
||||
@@ -77,43 +94,50 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitFilesView() error {
|
||||
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
currentSideContext := gui.currentSideContext()
|
||||
if currentSideContext.GetKey() == COMMIT_FILES_CONTEXT_KEY || currentSideContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
to := gui.State.Panels.CommitFiles.refName
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse, gui.GitCommand.PatchManager)
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.CommitFiles = files
|
||||
gui.State.CommitFileManager.SetFiles(files, to)
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleOpenOldCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openFile(file.Name)
|
||||
return gui.openFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleEditCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
if node.File == nil {
|
||||
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
|
||||
}
|
||||
|
||||
return gui.editFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
func (gui *Gui) handleToggleFileForPatch() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,18 +148,32 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name); err != nil {
|
||||
return err
|
||||
// if there is any file that hasn't been fully added we'll fully add everything,
|
||||
// otherwise we'll remove everything
|
||||
adding := node.AnyFile(func(file *models.CommitFile) bool {
|
||||
return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE
|
||||
})
|
||||
|
||||
err := node.ForEachFile(func(file *models.CommitFile) error {
|
||||
if adding {
|
||||
return gui.GitCommand.PatchManager.AddFileWhole(file.Name)
|
||||
} else {
|
||||
return gui.GitCommand.PatchManager.RemoveFile(file.Name)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
}
|
||||
|
||||
return gui.refreshCommitFilesView()
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@@ -159,16 +197,20 @@ func (gui *Gui) startPatchManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEnterCommitFile() error {
|
||||
return gui.enterCommitFile(-1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return gui.handleToggleCommitFileDirCollapsed()
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
@@ -176,13 +218,13 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.Contexts.PatchBuilding.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.PatchBuilding); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@@ -192,7 +234,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return enterTheFile(selectedLineIdx)
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.pushContext(gui.Contexts.CommitFiles.Context)
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -200,20 +242,60 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.CommitFileManager.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
|
||||
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
|
||||
// no longer considers the commitFiles view as its main view.
|
||||
gui.resetWindowForView("commitFiles")
|
||||
gui.resetWindowForView(gui.Views.CommitFiles)
|
||||
|
||||
gui.State.Panels.CommitFiles.SelectedLineIdx = 0
|
||||
gui.State.Panels.CommitFiles.refName = refName
|
||||
gui.State.Panels.CommitFiles.canRebase = canRebase
|
||||
gui.Contexts.CommitFiles.Context.SetParentContext(context)
|
||||
gui.Contexts.CommitFiles.Context.SetWindowName(windowName)
|
||||
gui.State.Contexts.CommitFiles.SetParentContext(context)
|
||||
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.pushContext(gui.Contexts.CommitFiles.Context)
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
|
||||
func (gui *Gui) handleToggleCommitFileTreeView() error {
|
||||
path := gui.getSelectedCommitFilePath()
|
||||
|
||||
gui.State.CommitFileManager.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.CommitFileManager.ExpandToPath(path)
|
||||
index, found := gui.State.CommitFileManager.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.State.Contexts.CommitFiles.GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.State.Contexts.CommitFiles.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,23 +11,19 @@ import (
|
||||
|
||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
||||
// needs to be set on the gui object, it does so, and then returns the error
|
||||
// the bool returned tells us whether the calling code should continue
|
||||
// needs to be run, it runs it
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return false, gui.Errors.ErrSubProcess
|
||||
return false, gui.runSubprocessWithSuspense(sub)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
func (gui *Gui) handleCommitConfirm() error {
|
||||
message := gui.trimmedContent(gui.Views.CommitMessage)
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
@@ -44,12 +40,12 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.clearEditorView(v)
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
_ = gui.returnFromContext()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitClose() error {
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
@@ -63,7 +59,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", message)
|
||||
gui.renderString(gui.Views.Options, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,6 +72,6 @@ func (gui *Gui) RenderCommitLength() {
|
||||
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
|
||||
return
|
||||
}
|
||||
v := gui.getCommitMessageView()
|
||||
v.Subtitle = gui.getBufferLength(v)
|
||||
|
||||
gui.Views.CommitMessage.Subtitle = gui.getBufferLength(gui.Views.CommitMessage)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gui
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
|
||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,12 +35,12 @@ func (gui *Gui) handleCommitSelect() error {
|
||||
var task updateTask
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
|
||||
)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -87,7 +86,7 @@ func (gui *Gui) refreshCommits() error {
|
||||
|
||||
go utils.Safe(func() {
|
||||
_ = gui.refreshCommitsWithLimit()
|
||||
context, ok := gui.Contexts.CommitFiles.Context.GetParentContext()
|
||||
context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
|
||||
if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
|
||||
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
|
||||
@@ -118,7 +117,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.Path,
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
},
|
||||
@@ -128,7 +127,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshRebaseCommits() error {
|
||||
@@ -143,12 +142,12 @@ func (gui *Gui) refreshRebaseCommits() error {
|
||||
}
|
||||
gui.State.Commits = updatedCommits
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitSquashDown() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -177,7 +176,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitFixup() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +205,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRenameCommit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -246,7 +245,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRenameCommitEditor() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -264,8 +263,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspense(subProcess)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -295,7 +293,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
return true, gui.refreshRebaseCommits()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitDelete() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -320,7 +318,7 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitMoveDown() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -347,7 +345,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitMoveUp() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -374,7 +372,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitEdit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -393,7 +391,7 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitAmendTo() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -410,7 +408,7 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitPick() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -425,10 +423,10 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
// at this point we aren't actually rebasing so we will interpret this as an
|
||||
// attempt to pull. We might revoke this later after enabling configurable keybindings
|
||||
return gui.handlePullFiles(g, v)
|
||||
return gui.handlePullFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitRevert() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -437,7 +435,7 @@ func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleViewCommitFiles() error {
|
||||
@@ -446,10 +444,10 @@ func (gui *Gui) handleViewCommitFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
|
||||
return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateFixupCommit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -479,7 +477,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -508,7 +506,7 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleTagCommit() error {
|
||||
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
|
||||
// if annotated, switch to a subprocess to create the message
|
||||
|
||||
@@ -527,12 +525,12 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckoutCommit() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
@@ -547,7 +545,7 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateCommitResetMenu() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
|
||||
@@ -556,30 +554,30 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleOpenSearchForCommitsPanel(_viewName string) error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleOpenSearch(gui.g, v)
|
||||
return gui.handleOpenSearch("commits")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel() error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
return context.handleGotoBottom(g, v)
|
||||
return context.handleGotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,19 +108,17 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteConfirmationView() {
|
||||
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.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
view := gui.getConfirmationView()
|
||||
if view == nil {
|
||||
return nil // if it's already been closed we can just return
|
||||
// we've already closed it so we can just return
|
||||
if !gui.Views.Confirmation.Visible {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !handlersManageFocus {
|
||||
@@ -129,9 +127,9 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.deleteConfirmationView()
|
||||
|
||||
_, _ = gui.g.SetViewOnBottom("suggestions")
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
gui.Views.Confirmation.Visible = false
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -172,67 +170,64 @@ 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) (*gocui.View, error) {
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
// calling SetView on an existing view returns the same view, so I'm not bothering
|
||||
// to reassign to gui.Views.Confirmation
|
||||
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return nil, err
|
||||
}
|
||||
confirmationView.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
confirmationView.Title = title
|
||||
confirmationView.Wrap = true
|
||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
gui.Views.Confirmation.Title = title
|
||||
gui.Views.Confirmation.Wrap = true
|
||||
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)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return nil, err
|
||||
}
|
||||
suggestionsView.Wrap = true
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
return err
|
||||
}
|
||||
suggestionsView.Wrap = true
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions([]*types.Suggestion{})
|
||||
_, _ = gui.g.SetViewOnTop("suggestions")
|
||||
suggestionsView.Visible = true
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.pushContext(gui.Contexts.Confirmation.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Confirmation)
|
||||
})
|
||||
return confirmationView, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
// delete the existing confirmation panel if it exists
|
||||
if view, _ := g.View("confirmation"); view != nil {
|
||||
gui.deleteConfirmationView()
|
||||
}
|
||||
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
||||
// remove any previous keybindings
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
|
||||
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = opts.editable
|
||||
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
if opts.editable {
|
||||
go utils.Safe(func() {
|
||||
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
|
||||
time.Sleep(time.Millisecond)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
confirmationView.EditGotoToEndOfLine()
|
||||
gui.Views.Confirmation.EditGotoToEndOfLine()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
gui.renderString("confirmation", opts.prompt)
|
||||
gui.renderString(gui.Views.Confirmation, opts.prompt)
|
||||
|
||||
return gui.setKeyBindings(opts)
|
||||
})
|
||||
@@ -248,10 +243,10 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", actions)
|
||||
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.getConfirmationView().Buffer() })
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
|
||||
} else {
|
||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||
}
|
||||
@@ -284,7 +279,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.Contexts.Suggestions.Context) },
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
@@ -304,7 +299,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
{
|
||||
viewName: "suggestions",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.Contexts.Confirmation.Context) },
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -317,6 +312,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
@@ -335,11 +336,5 @@ func (gui *Gui) surfaceError(err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, sentinelError := range gui.sentinelErrorsArr() {
|
||||
if err == sentinelError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
@@ -6,39 +6,43 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type ContextKind int
|
||||
|
||||
const (
|
||||
SIDE_CONTEXT int = iota
|
||||
SIDE_CONTEXT ContextKind = iota
|
||||
MAIN_CONTEXT
|
||||
TEMPORARY_POPUP
|
||||
PERSISTENT_POPUP
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
STATUS_CONTEXT_KEY = "status"
|
||||
FILES_CONTEXT_KEY = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY = "localBranches"
|
||||
REMOTES_CONTEXT_KEY = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY = "commitFiles"
|
||||
STASH_CONTEXT_KEY = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY = "staging"
|
||||
MENU_CONTEXT_KEY = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY = "confirmation"
|
||||
SEARCH_CONTEXT_KEY = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY = "suggestions"
|
||||
STATUS_CONTEXT_KEY ContextKey = "status"
|
||||
FILES_CONTEXT_KEY ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY ContextKey = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY ContextKey = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY ContextKey = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
|
||||
MENU_CONTEXT_KEY ContextKey = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
|
||||
)
|
||||
|
||||
var allContextKeys = []string{
|
||||
var allContextKeys = []ContextKey{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
@@ -63,62 +67,54 @@ var allContextKeys = []string{
|
||||
SUGGESTIONS_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
type SimpleContextNode struct {
|
||||
Context Context
|
||||
}
|
||||
|
||||
type RemotesContextNode struct {
|
||||
Context Context
|
||||
Branches SimpleContextNode
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Status SimpleContextNode
|
||||
Files SimpleContextNode
|
||||
Submodules SimpleContextNode
|
||||
Menu SimpleContextNode
|
||||
Branches SimpleContextNode
|
||||
Remotes RemotesContextNode
|
||||
Tags SimpleContextNode
|
||||
BranchCommits SimpleContextNode
|
||||
CommitFiles SimpleContextNode
|
||||
ReflogCommits SimpleContextNode
|
||||
SubCommits SimpleContextNode
|
||||
Stash SimpleContextNode
|
||||
Normal SimpleContextNode
|
||||
Staging SimpleContextNode
|
||||
PatchBuilding SimpleContextNode
|
||||
Merging SimpleContextNode
|
||||
Credentials SimpleContextNode
|
||||
Confirmation SimpleContextNode
|
||||
CommitMessage SimpleContextNode
|
||||
Search SimpleContextNode
|
||||
Suggestions SimpleContextNode
|
||||
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
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
Merging Context
|
||||
Credentials Context
|
||||
Confirmation Context
|
||||
CommitMessage Context
|
||||
Search Context
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []Context{
|
||||
gui.Contexts.Status.Context,
|
||||
gui.Contexts.Files.Context,
|
||||
gui.Contexts.Submodules.Context,
|
||||
gui.Contexts.Branches.Context,
|
||||
gui.Contexts.Remotes.Context,
|
||||
gui.Contexts.Remotes.Branches.Context,
|
||||
gui.Contexts.Tags.Context,
|
||||
gui.Contexts.BranchCommits.Context,
|
||||
gui.Contexts.CommitFiles.Context,
|
||||
gui.Contexts.ReflogCommits.Context,
|
||||
gui.Contexts.Stash.Context,
|
||||
gui.Contexts.Menu.Context,
|
||||
gui.Contexts.Confirmation.Context,
|
||||
gui.Contexts.Credentials.Context,
|
||||
gui.Contexts.CommitMessage.Context,
|
||||
gui.Contexts.Normal.Context,
|
||||
gui.Contexts.Staging.Context,
|
||||
gui.Contexts.Merging.Context,
|
||||
gui.Contexts.PatchBuilding.Context,
|
||||
gui.Contexts.SubCommits.Context,
|
||||
gui.Contexts.Suggestions.Context,
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Confirmation,
|
||||
gui.State.Contexts.Credentials,
|
||||
gui.State.Contexts.CommitMessage,
|
||||
gui.State.Contexts.Normal,
|
||||
gui.State.Contexts.Staging,
|
||||
gui.State.Contexts.Merging,
|
||||
gui.State.Contexts.PatchBuilding,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +122,11 @@ type Context interface {
|
||||
HandleFocus() error
|
||||
HandleFocusLost() error
|
||||
HandleRender() error
|
||||
GetKind() int
|
||||
GetKind() ContextKind
|
||||
GetViewName() string
|
||||
GetWindowName() string
|
||||
SetWindowName(string)
|
||||
GetKey() string
|
||||
GetKey() ContextKey
|
||||
SetParentContext(Context)
|
||||
|
||||
// we return a bool here to tell us whether or not the returned value just wraps a nil
|
||||
@@ -143,8 +139,8 @@ type BasicContext struct {
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
Kind int
|
||||
Key string
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
}
|
||||
|
||||
@@ -194,206 +190,176 @@ func (c BasicContext) HandleFocusLost() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKind() int {
|
||||
func (c BasicContext) GetKind() ContextKind {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKey() string {
|
||||
func (c BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
Status: BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
Files: gui.filesListContext(),
|
||||
Submodules: gui.submodulesListContext(),
|
||||
Menu: gui.menuListContext(),
|
||||
Remotes: gui.remotesListContext(),
|
||||
RemoteBranches: gui.remoteBranchesListContext(),
|
||||
BranchCommits: gui.branchCommitsListContext(),
|
||||
CommitFiles: gui.commitFilesListContext(),
|
||||
ReflogCommits: gui.reflogCommitsListContext(),
|
||||
SubCommits: gui.subCommitsListContext(),
|
||||
Branches: gui.branchesListContext(),
|
||||
Tags: gui.tagsListContext(),
|
||||
Stash: gui.stashListContext(),
|
||||
Normal: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
},
|
||||
Files: SimpleContextNode{
|
||||
Context: gui.filesListContext(),
|
||||
},
|
||||
Submodules: SimpleContextNode{
|
||||
Context: gui.submodulesListContext(),
|
||||
},
|
||||
Menu: SimpleContextNode{
|
||||
Context: gui.menuListContext(),
|
||||
},
|
||||
Remotes: RemotesContextNode{
|
||||
Context: gui.remotesListContext(),
|
||||
Branches: SimpleContextNode{
|
||||
Context: gui.remoteBranchesListContext(),
|
||||
Staging: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_STAGING_CONTEXT_KEY,
|
||||
},
|
||||
BranchCommits: SimpleContextNode{
|
||||
Context: gui.branchCommitsListContext(),
|
||||
},
|
||||
CommitFiles: SimpleContextNode{
|
||||
Context: gui.commitFilesListContext(),
|
||||
},
|
||||
ReflogCommits: SimpleContextNode{
|
||||
Context: gui.reflogCommitsListContext(),
|
||||
},
|
||||
SubCommits: SimpleContextNode{
|
||||
Context: gui.subCommitsListContext(),
|
||||
},
|
||||
Branches: SimpleContextNode{
|
||||
Context: gui.branchesListContext(),
|
||||
},
|
||||
Tags: SimpleContextNode{
|
||||
Context: gui.tagsListContext(),
|
||||
},
|
||||
Stash: SimpleContextNode{
|
||||
Context: gui.stashListContext(),
|
||||
},
|
||||
Normal: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
PatchBuilding: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshPatchBuildingPanel(-1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
Staging: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_STAGING_CONTEXT_KEY,
|
||||
},
|
||||
Merging: BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
PatchBuilding: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshPatchBuildingPanel(-1)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
Credentials: BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
Merging: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.refreshMergePanel,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Confirmation: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
Credentials: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
Suggestions: gui.suggestionsListContext(),
|
||||
CommitMessage: BasicContext{
|
||||
OnFocus: gui.handleCommitMessageFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
Confirmation: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Suggestions: SimpleContextNode{
|
||||
Context: gui.suggestionsListContext(),
|
||||
},
|
||||
CommitMessage: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.handleCommitMessageFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
},
|
||||
Search: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
Search: BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) initialViewContextMap() map[string]Context {
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": gui.Contexts.Status.Context,
|
||||
"files": gui.Contexts.Files.Context,
|
||||
"branches": gui.Contexts.Branches.Context,
|
||||
"commits": gui.Contexts.BranchCommits.Context,
|
||||
"commitFiles": gui.Contexts.CommitFiles.Context,
|
||||
"stash": gui.Contexts.Stash.Context,
|
||||
"menu": gui.Contexts.Menu.Context,
|
||||
"confirmation": gui.Contexts.Confirmation.Context,
|
||||
"credentials": gui.Contexts.Credentials.Context,
|
||||
"commitMessage": gui.Contexts.CommitMessage.Context,
|
||||
"main": gui.Contexts.Normal.Context,
|
||||
"secondary": gui.Contexts.Normal.Context,
|
||||
"status": tree.Status,
|
||||
"files": tree.Files,
|
||||
"branches": tree.Branches,
|
||||
"commits": tree.BranchCommits,
|
||||
"commitFiles": tree.CommitFiles,
|
||||
"stash": tree.Stash,
|
||||
"menu": tree.Menu,
|
||||
"confirmation": tree.Confirmation,
|
||||
"credentials": tree.Credentials,
|
||||
"commitMessage": tree.CommitMessage,
|
||||
"main": tree.Normal,
|
||||
"secondary": tree.Normal,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabContextMap() map[string][]tabContext {
|
||||
func (gui *Gui) popupViewNames() []string {
|
||||
result := []string{}
|
||||
for _, context := range gui.allContexts() {
|
||||
if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
|
||||
result = append(result, context.GetViewName())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{gui.Contexts.Branches.Context},
|
||||
contexts: []Context{tree.Branches},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Remotes.Context,
|
||||
gui.Contexts.Remotes.Branches.Context,
|
||||
tree.Remotes,
|
||||
tree.RemoteBranches,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{gui.Contexts.Tags.Context},
|
||||
contexts: []Context{tree.Tags},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{gui.Contexts.BranchCommits.Context},
|
||||
contexts: []Context{tree.BranchCommits},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
gui.Contexts.ReflogCommits.Context,
|
||||
tree.ReflogCommits,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{gui.Contexts.Files.Context},
|
||||
contexts: []Context{tree.Files},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Submodules.Context,
|
||||
tree.Submodules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
stack := gui.State.ContextStack
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
for i := range stack {
|
||||
reversedIndex := len(stack) - 1 - i
|
||||
@@ -411,11 +377,14 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
// hitting escape: you want to go that context's parent instead.
|
||||
func (gui *Gui) replaceContext(c Context) error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if len(gui.State.ContextStack) == 0 {
|
||||
gui.State.ContextStack = []Context{c}
|
||||
gui.State.ContextManager.Lock()
|
||||
defer gui.State.ContextManager.Unlock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||
gui.State.ContextManager.ContextStack = []Context{c}
|
||||
} else {
|
||||
// replace the last item with the given item
|
||||
gui.State.ContextStack = append(gui.State.ContextStack[0:len(gui.State.ContextStack)-1], c)
|
||||
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
|
||||
}
|
||||
|
||||
return gui.activateContext(c)
|
||||
@@ -426,28 +395,42 @@ func (gui *Gui) replaceContext(c Context) error {
|
||||
|
||||
func (gui *Gui) pushContext(c Context) error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// push onto stack
|
||||
// if we are switching to a side context, remove all other contexts in the stack
|
||||
if c.GetKind() == SIDE_CONTEXT {
|
||||
for _, stackContext := range gui.State.ContextStack {
|
||||
if stackContext.GetKey() != c.GetKey() {
|
||||
if err := gui.deactivateContext(stackContext); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.State.ContextStack = []Context{c}
|
||||
} else {
|
||||
// TODO: think about other exceptional cases
|
||||
gui.State.ContextStack = append(gui.State.ContextStack, c)
|
||||
}
|
||||
|
||||
return gui.activateContext(c)
|
||||
return gui.pushContextDirect(c)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushContextDirect(c Context) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
// push onto stack
|
||||
// if we are switching to a side context, remove all other contexts in the stack
|
||||
if c.GetKind() == SIDE_CONTEXT {
|
||||
for _, stackContext := range gui.State.ContextManager.ContextStack {
|
||||
if stackContext.GetKey() != c.GetKey() {
|
||||
if err := gui.deactivateContext(stackContext); err != nil {
|
||||
gui.State.ContextManager.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.State.ContextManager.ContextStack = []Context{c}
|
||||
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1].GetKey() != c.GetKey() {
|
||||
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
|
||||
// In that case we'll just close the menu entirely when the user hits escape.
|
||||
|
||||
// TODO: think about other exceptional cases
|
||||
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
|
||||
}
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
return gui.activateContext(c)
|
||||
}
|
||||
|
||||
// asynchronous code idea: functions return an error via a channel, when done
|
||||
|
||||
// pushContextWithView is to be used when you don't know which context you
|
||||
// want to switch to: you only know the view that you want to switch to. It will
|
||||
// look up the context currently active for that view and switch to that context
|
||||
@@ -457,19 +440,22 @@ func (gui *Gui) pushContextWithView(viewName string) error {
|
||||
|
||||
func (gui *Gui) returnFromContext() error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// TODO: add mutexes
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextStack) == 1 {
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextStack) - 1
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextStack[n]
|
||||
newContext := gui.State.ContextStack[n-1]
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextStack = gui.State.ContextStack[:n]
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
@@ -482,9 +468,17 @@ func (gui *Gui) returnFromContext() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c Context) error {
|
||||
view, _ := gui.g.View(c.GetViewName())
|
||||
|
||||
if view != nil && view.IsSearching() {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if we are the kind of context that is sent to back upon deactivation, we should do that
|
||||
if c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP {
|
||||
_, _ = gui.g.SetViewOnBottom(c.GetViewName())
|
||||
if view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
|
||||
view.Visible = false
|
||||
}
|
||||
|
||||
if err := c.HandleFocusLost(); err != nil {
|
||||
@@ -503,7 +497,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Context != c.GetKey() {
|
||||
if ContextKey(v.Context) != c.GetKey() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -523,14 +517,13 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
func (gui *Gui) activateContext(c Context) error {
|
||||
viewName := c.GetViewName()
|
||||
v, err := gui.g.View(viewName)
|
||||
// if view no longer exists, pop again
|
||||
if err != nil {
|
||||
return gui.returnFromContext()
|
||||
return err
|
||||
}
|
||||
originalViewContextKey := v.Context
|
||||
originalViewContextKey := ContextKey(v.Context)
|
||||
|
||||
// ensure that any other window for which this view was active is now set to the default for that window.
|
||||
gui.setViewAsActiveForWindow(viewName)
|
||||
gui.setViewAsActiveForWindow(v)
|
||||
|
||||
if viewName == "main" {
|
||||
gui.changeMainViewsContext(c.GetKey())
|
||||
@@ -541,14 +534,10 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
gui.setViewTabForContext(c)
|
||||
|
||||
if _, err := gui.g.SetCurrentView(viewName); err != nil {
|
||||
// if view no longer exists, pop again
|
||||
return gui.returnFromContext()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
|
||||
// if view no longer exists, pop again
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
v.Visible = true
|
||||
|
||||
// if the new context's view was previously displaying another context, render the new context
|
||||
if originalViewContextKey != c.GetKey() {
|
||||
@@ -557,7 +546,7 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
v.Context = c.GetKey()
|
||||
v.Context = string(c.GetKey())
|
||||
|
||||
gui.g.Cursor = v.Editable
|
||||
|
||||
@@ -578,29 +567,46 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// currently unused
|
||||
// // currently unused
|
||||
// func (gui *Gui) renderContextStack() string {
|
||||
// result := ""
|
||||
// for _, context := range gui.State.ContextStack {
|
||||
// result += context.GetKey() + "\n"
|
||||
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||
// result += string(context.GetKey()) + "\n"
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
func (gui *Gui) currentContext() Context {
|
||||
if len(gui.State.ContextStack) == 0 {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
|
||||
return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideContext() *ListContext {
|
||||
stack := gui.State.ContextStack
|
||||
// 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 {
|
||||
context := gui.currentSideContext()
|
||||
listContext, ok := context.(*ListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return listContext
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
// on startup the stack can be empty so we'll return an empty string in that case
|
||||
if len(stack) == 0 {
|
||||
return nil
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// find the first context in the stack with the type of SIDE_CONTEXT
|
||||
@@ -608,22 +614,22 @@ func (gui *Gui) currentSideContext() *ListContext {
|
||||
context := stack[len(stack)-1-i]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
// not all side contexts are list contexts (e.g. the status panel)
|
||||
listContext, ok := context.(*ListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return listContext
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultSideContext() Context {
|
||||
return gui.Contexts.Files.Context
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
return gui.State.Contexts.BranchCommits
|
||||
} else {
|
||||
return gui.State.Contexts.Files
|
||||
}
|
||||
}
|
||||
|
||||
// remove the need to do this: always use a mapping
|
||||
func (gui *Gui) setInitialViewContexts() {
|
||||
// arguably we should only have our ViewContextMap and we should do away with
|
||||
// contexts on views, or vice versa
|
||||
@@ -634,7 +640,7 @@ func (gui *Gui) setInitialViewContexts() {
|
||||
continue
|
||||
}
|
||||
|
||||
view.Context = context.GetKey()
|
||||
view.Context = string(context.GetKey())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,13 +675,14 @@ func (gui *Gui) onViewFocusChange() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
if v == nil {
|
||||
func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error {
|
||||
if oldView == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.IsSearching() && newView.Name() != "search" {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -687,15 +694,15 @@ func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
// which currently just means a context that affects both the main and secondary views
|
||||
// other views can have their context changed directly but this function helps
|
||||
// keep the main and secondary views in sync
|
||||
func (gui *Gui) changeMainViewsContext(contextKey string) {
|
||||
func (gui *Gui) changeMainViewsContext(contextKey ContextKey) {
|
||||
if gui.State.MainContext == contextKey {
|
||||
return
|
||||
}
|
||||
|
||||
switch contextKey {
|
||||
case MAIN_NORMAL_CONTEXT_KEY, MAIN_PATCH_BUILDING_CONTEXT_KEY, MAIN_STAGING_CONTEXT_KEY, MAIN_MERGING_CONTEXT_KEY:
|
||||
gui.getMainView().Context = contextKey
|
||||
gui.getSecondaryView().Context = contextKey
|
||||
gui.Views.Main.Context = string(contextKey)
|
||||
gui.Views.Secondary.Context = string(contextKey)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
|
||||
}
|
||||
@@ -704,7 +711,7 @@ func (gui *Gui) changeMainViewsContext(contextKey string) {
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabNames(viewName string) []string {
|
||||
tabContexts := gui.ViewTabContextMap[viewName]
|
||||
tabContexts := gui.State.ViewTabContextMap[viewName]
|
||||
|
||||
if len(tabContexts) == 0 {
|
||||
return nil
|
||||
@@ -720,7 +727,7 @@ func (gui *Gui) viewTabNames(viewName string) []string {
|
||||
|
||||
func (gui *Gui) setViewTabForContext(c Context) {
|
||||
// search for the context in our map and if we find it, set the tab for the corresponding view
|
||||
tabContexts, ok := gui.ViewTabContextMap[c.GetViewName()]
|
||||
tabContexts, ok := gui.State.ViewTabContextMap[c.GetViewName()]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -746,7 +753,7 @@ type tabContext struct {
|
||||
contexts []Context
|
||||
}
|
||||
|
||||
func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
||||
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
|
||||
context, ok := gui.contextForContextKey(contextKey)
|
||||
|
||||
if !ok {
|
||||
@@ -756,7 +763,7 @@ func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
||||
return context
|
||||
}
|
||||
|
||||
func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
||||
func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
|
||||
for _, context := range gui.allContexts() {
|
||||
if context.GetKey() == contextKey {
|
||||
return context, true
|
||||
@@ -766,18 +773,28 @@ func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderView(viewName string) error {
|
||||
v, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextKey := v.Context
|
||||
func (gui *Gui) rerenderView(view *gocui.View) error {
|
||||
contextKey := ContextKey(view.Context)
|
||||
context := gui.mustContextForContextKey(contextKey)
|
||||
|
||||
return context.HandleRender()
|
||||
}
|
||||
|
||||
func (gui *Gui) getSideContextSelectedItemId() string {
|
||||
currentSideContext := gui.currentSideListContext()
|
||||
if currentSideContext == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
item, ok := currentSideContext.GetSelectedItem()
|
||||
|
||||
if ok {
|
||||
return item.ID()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// currently unused
|
||||
// func (gui *Gui) getCurrentSideView() *gocui.View {
|
||||
// currentSideContext := gui.currentSideContext()
|
||||
@@ -790,17 +807,11 @@ func (gui *Gui) rerenderView(viewName string) error {
|
||||
// return view
|
||||
// }
|
||||
|
||||
func (gui *Gui) getSideContextSelectedItemId() string {
|
||||
currentSideContext := gui.currentSideContext()
|
||||
if currentSideContext == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
item, ok := currentSideContext.GetSelectedItem()
|
||||
|
||||
if ok {
|
||||
return item.ID()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
// currently unused
|
||||
// func (gui *Gui) renderContextStack() string {
|
||||
// result := ""
|
||||
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||
// result += context.GetViewName() + "\n"
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
@@ -13,7 +13,7 @@ type credentials chan string
|
||||
func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
gui.credentials = make(chan string)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
credentialsView, _ := g.View("credentials")
|
||||
credentialsView := gui.Views.Credentials
|
||||
switch passOrUname {
|
||||
case "username":
|
||||
credentialsView.Title = gui.Tr.CredentialsUsername
|
||||
@@ -26,7 +26,7 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
credentialsView.Mask = '*'
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.Contexts.Credentials.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Credentials); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -39,10 +39,11 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
return userInput + "\n"
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
func (gui *Gui) handleSubmitCredential() error {
|
||||
credentialsView := gui.Views.Credentials
|
||||
message := gui.trimmedContent(credentialsView)
|
||||
gui.credentials <- message
|
||||
gui.clearEditorView(v)
|
||||
gui.clearEditorView(credentialsView)
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCloseCredentialsView() error {
|
||||
gui.credentials <- ""
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
@@ -66,7 +67,7 @@ func (gui *Gui) handleCredentialsViewFocused() error {
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", message)
|
||||
gui.renderString(gui.Views.Options, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -12,34 +12,38 @@ import (
|
||||
)
|
||||
|
||||
type CustomCommandObjects struct {
|
||||
SelectedLocalCommit *models.Commit
|
||||
SelectedReflogCommit *models.Commit
|
||||
SelectedSubCommit *models.Commit
|
||||
SelectedFile *models.File
|
||||
SelectedLocalBranch *models.Branch
|
||||
SelectedRemoteBranch *models.RemoteBranch
|
||||
SelectedRemote *models.Remote
|
||||
SelectedTag *models.Tag
|
||||
SelectedStashEntry *models.StashEntry
|
||||
SelectedCommitFile *models.CommitFile
|
||||
CheckedOutBranch *models.Branch
|
||||
PromptResponses []string
|
||||
SelectedLocalCommit *models.Commit
|
||||
SelectedReflogCommit *models.Commit
|
||||
SelectedSubCommit *models.Commit
|
||||
SelectedFile *models.File
|
||||
SelectedPath string
|
||||
SelectedLocalBranch *models.Branch
|
||||
SelectedRemoteBranch *models.RemoteBranch
|
||||
SelectedRemote *models.Remote
|
||||
SelectedTag *models.Tag
|
||||
SelectedStashEntry *models.StashEntry
|
||||
SelectedCommitFile *models.CommitFile
|
||||
SelectedCommitFilePath string
|
||||
CheckedOutBranch *models.Branch
|
||||
PromptResponses []string
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
||||
objects := CustomCommandObjects{
|
||||
SelectedFile: gui.getSelectedFile(),
|
||||
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
||||
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
||||
SelectedLocalBranch: gui.getSelectedBranch(),
|
||||
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
||||
SelectedRemote: gui.getSelectedRemote(),
|
||||
SelectedTag: gui.getSelectedTag(),
|
||||
SelectedStashEntry: gui.getSelectedStashEntry(),
|
||||
SelectedCommitFile: gui.getSelectedCommitFile(),
|
||||
SelectedSubCommit: gui.getSelectedSubCommit(),
|
||||
CheckedOutBranch: gui.currentBranch(),
|
||||
PromptResponses: promptResponses,
|
||||
SelectedFile: gui.getSelectedFile(),
|
||||
SelectedPath: gui.getSelectedPath(),
|
||||
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
||||
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
||||
SelectedLocalBranch: gui.getSelectedBranch(),
|
||||
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
||||
SelectedRemote: gui.getSelectedRemote(),
|
||||
SelectedTag: gui.getSelectedTag(),
|
||||
SelectedStashEntry: gui.getSelectedStashEntry(),
|
||||
SelectedCommitFile: gui.getSelectedCommitFile(),
|
||||
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
|
||||
SelectedSubCommit: gui.getSelectedSubCommit(),
|
||||
CheckedOutBranch: gui.currentBranch(),
|
||||
PromptResponses: promptResponses,
|
||||
}
|
||||
|
||||
return utils.ResolveTemplate(templateStr, objects)
|
||||
@@ -56,8 +60,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
}
|
||||
|
||||
if customCommand.Subprocess {
|
||||
gui.PrepareSubProcess(cmdStr)
|
||||
return nil
|
||||
return gui.runSubprocessWithSuspense(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
}
|
||||
|
||||
loadingText := customCommand.LoadingText
|
||||
@@ -65,9 +68,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
||||
}
|
||||
return gui.WithWaitingStatus(loadingText, func() error {
|
||||
gui.OSCommand.PrepareSubProcess(cmdStr)
|
||||
|
||||
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
||||
if err := gui.OSCommand.RunShellCommand(cmdStr); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
@@ -175,9 +176,14 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
||||
case "":
|
||||
log.Fatalf("Error parsing custom command keybindings: context not provided (use context: 'global' for the global context). Key: %s, Command: %s", customCommand.Key, customCommand.Command)
|
||||
default:
|
||||
context, ok := gui.contextForContextKey(customCommand.Context)
|
||||
context, ok := gui.contextForContextKey(ContextKey(customCommand.Context))
|
||||
// stupid golang making me build an array of strings for this.
|
||||
allContextKeyStrings := make([]string, len(allContextKeys))
|
||||
for i := range allContextKeys {
|
||||
allContextKeyStrings[i] = string(allContextKeys[i])
|
||||
}
|
||||
if !ok {
|
||||
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeys, ", "))
|
||||
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeyStrings, ", "))
|
||||
}
|
||||
// here we assume that a given context will always belong to the same view.
|
||||
// Currently this is a safe bet but it's by no means guaranteed in the long term
|
||||
@@ -196,7 +202,7 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
||||
Contexts: contexts,
|
||||
Key: gui.getKey(customCommand.Key),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.wrappedHandler(gui.handleCustomCommandKeybinding(customCommand)),
|
||||
Handler: gui.handleCustomCommandKeybinding(customCommand),
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
@@ -16,7 +14,7 @@ func (gui *Gui) renderDiff() error {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -51,7 +49,7 @@ func (gui *Gui) currentDiffTerminals() []string {
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -96,17 +94,13 @@ func (gui *Gui) diffStr() string {
|
||||
if file != "" {
|
||||
output += " -- " + file
|
||||
} else if gui.State.Modes.Filtering.Active() {
|
||||
output += " -- " + gui.State.Modes.Filtering.Path
|
||||
output += " -- " + gui.State.Modes.Filtering.GetPath()
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel() error {
|
||||
names := gui.currentDiffTerminals()
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
|
||||
@@ -1,56 +1,79 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var menuItems []*menuItem
|
||||
|
||||
submodules := gui.State.Submodules
|
||||
if file.IsSubmodule(submodules) {
|
||||
submodule := file.SubmoduleConfig(submodules)
|
||||
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcSubmoduleStashAndReset,
|
||||
onPress: func() error {
|
||||
return gui.handleResetSubmodule(submodule)
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if node.File == nil {
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
file := node.File
|
||||
|
||||
submodules := gui.State.Submodules
|
||||
if file.IsSubmodule(submodules) {
|
||||
submodule := file.SubmoduleConfig(submodules)
|
||||
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcSubmoduleStashAndReset,
|
||||
onPress: func() error {
|
||||
return gui.handleResetSubmodule(submodule)
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
|
||||
return gui.createMenu(node.GetPath(), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"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) {
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyTab
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
@@ -37,18 +40,25 @@ func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod g
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
default:
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
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.KeyDelete:
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
@@ -68,8 +78,12 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
default:
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if gui.findSuggestions != nil {
|
||||
@@ -77,4 +91,6 @@ func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.M
|
||||
suggestions := gui.findSuggestions(input)
|
||||
gui.setSuggestions(suggestions)
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
3
pkg/gui/errors.go
Normal file
3
pkg/gui/errors.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package gui
|
||||
|
||||
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
|
||||
@@ -117,7 +117,7 @@ func (gui *Gui) watchFilesForChanges() {
|
||||
}
|
||||
// only refresh if we're not already
|
||||
if !gui.State.IsRefreshingFiles {
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
// watch for errors
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -15,65 +9,86 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
func (gui *Gui) getSelectedFileNode() *filetree.FileNode {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Files[selectedLine]
|
||||
return gui.State.FileManager.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedPath() string {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
|
||||
if node == nil {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(gui.Tr.NoChangedFiles),
|
||||
task: NewRenderStringTask(gui.Tr.NoChangedFiles),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
// TODO: pull into update task interface
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanel()
|
||||
if node.File != nil && node.File.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanelWithLock()
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
|
||||
title: gui.Tr.UnstagedChanges,
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
task: NewRunPtyTask(cmd),
|
||||
}}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if node.GetHasStagedChanges() {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.StagedChanges,
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.StagedChanges,
|
||||
task: NewRunPtyTask(cmd),
|
||||
}
|
||||
}
|
||||
} else if !file.HasUnstagedChanges {
|
||||
} else {
|
||||
refreshOpts.main.title = gui.Tr.StagedChanges
|
||||
}
|
||||
|
||||
@@ -88,13 +103,8 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
gui.Mutexes.RefreshingFilesMutex.Unlock()
|
||||
}()
|
||||
|
||||
selectedFile := gui.getSelectedFile()
|
||||
selectedPath := gui.getSelectedPath()
|
||||
|
||||
filesView := gui.getFilesView()
|
||||
if filesView == nil {
|
||||
// if the filesView hasn't been instantiated yet we just return
|
||||
return nil
|
||||
}
|
||||
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,20 +113,20 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if err := gui.postRefreshUpdate(gui.Contexts.Submodules.Context); err != nil {
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
if gui.getFilesView().Context == FILES_CONTEXT_KEY {
|
||||
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
|
||||
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
|
||||
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
|
||||
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedFile := gui.getSelectedFile()
|
||||
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
|
||||
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.Views.Main && ContextKey(g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedPath := gui.getSelectedPath()
|
||||
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
|
||||
if err := gui.selectFile(alreadySelected); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +141,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) stagedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
result := make([]*models.File, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
@@ -142,7 +152,7 @@ func (gui *Gui) stagedFiles() []*models.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) trackedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
result := make([]*models.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
@@ -161,16 +171,22 @@ func (gui *Gui) stageSelectedFile() error {
|
||||
return gui.GitCommand.StageFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEnterFile() error {
|
||||
return gui.enterFile(false, -1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return gui.handleToggleDirCollapsed()
|
||||
}
|
||||
|
||||
file := node.File
|
||||
|
||||
submoduleConfigs := gui.State.Submodules
|
||||
if file.IsSubmodule(submoduleConfigs) {
|
||||
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||
@@ -183,32 +199,53 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
|
||||
}
|
||||
_ = gui.pushContext(gui.Contexts.Staging.Context)
|
||||
_ = gui.pushContext(gui.State.Contexts.Staging)
|
||||
|
||||
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
if node.IsLeaf() {
|
||||
file := node.File
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
if err := gui.GitCommand.StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
if err := gui.GitCommand.StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
// if any files within have inline merge conflicts we can't stage or unstage,
|
||||
// or it'll end up with those >>>>>> lines actually staged
|
||||
if node.GetHasInlineMergeConflicts() {
|
||||
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
|
||||
}
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if err := gui.GitCommand.StageFile(node.Path); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
// pretty sure it doesn't matter that we're always passing true here
|
||||
if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -216,7 +253,7 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
@@ -228,7 +265,7 @@ func (gui *Gui) focusAndSelectFile() error {
|
||||
return gui.selectFile(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStageAll() error {
|
||||
var err error
|
||||
if gui.allFilesStaged() {
|
||||
err = gui.GitCommand.UnstageAll()
|
||||
@@ -239,53 +276,76 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.selectFile(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleIgnoreFile() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if file.Name == ".gitignore" {
|
||||
|
||||
if node.GetPath() == ".gitignore" {
|
||||
return gui.createErrorPanel("Cannot ignore .gitignore")
|
||||
}
|
||||
|
||||
if file.Tracked {
|
||||
unstageFiles := func() error {
|
||||
return node.ForEachFile(func(file *models.File) error {
|
||||
if file.HasStagedChanges {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if node.GetIsTracked() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.IgnoreTracked,
|
||||
prompt: gui.Tr.IgnoreTrackedPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
// not 100% sure if this is necessary but I'll assume it is
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
|
||||
|
||||
if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
|
||||
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
func (gui *Gui) handleWIPCommitPress() error {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPreifx == "" {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
_ = gui.renderStringSync("commitMessage", skipHookPreifx)
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
_ = gui.renderStringSync(gui.Views.CommitMessage, skipHookPreifx)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -320,11 +380,14 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
|
||||
}
|
||||
|
||||
commitMessageView := gui.getCommitMessageView()
|
||||
commitPrefixConfig := gui.commitPrefixConfigForRepo()
|
||||
if commitPrefixConfig != nil {
|
||||
prefixPattern := commitPrefixConfig.Pattern
|
||||
@@ -334,14 +397,14 @@ 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("commitMessage", prefix)
|
||||
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
|
||||
gui.renderString(gui.Views.CommitMessage, prefix)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if err := gui.pushContext(gui.Contexts.CommitMessage.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.CommitMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -369,6 +432,10 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress() error {
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
|
||||
}
|
||||
@@ -399,21 +466,17 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (gui *Gui) handleCommitEditorPress() error {
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
|
||||
}
|
||||
|
||||
gui.PrepareSubProcess("git commit")
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
|
||||
func (gui *Gui) PrepareSubProcess(command string) {
|
||||
splitCmd := str.ToArgv(command)
|
||||
gui.SubProcess = gui.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "commit"),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
@@ -421,58 +484,126 @@ func (gui *Gui) editFile(filename string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleFileEdit() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
if node.File == nil {
|
||||
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
|
||||
}
|
||||
|
||||
return gui.editFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleFileOpen() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
|
||||
return gui.openFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
func (gui *Gui) handleRefreshFiles() error {
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateFiles() error {
|
||||
state := gui.State
|
||||
|
||||
// keep track of where the cursor is currently and the current file names
|
||||
// when we refresh, go looking for a matching name
|
||||
// move the cursor to there.
|
||||
selectedFile := gui.getSelectedFile()
|
||||
|
||||
selectedNode := gui.getSelectedFileNode()
|
||||
|
||||
prevNodes := gui.State.FileManager.GetAllItems()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
|
||||
|
||||
// for when you stage the old file of a rename and the new file is in a collapsed dir
|
||||
state.FileManager.RWMutex.Lock()
|
||||
for _, file := range files {
|
||||
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
|
||||
state.FileManager.ExpandToPath(file.Name)
|
||||
}
|
||||
}
|
||||
|
||||
state.FileManager.SetFiles(files)
|
||||
state.FileManager.RWMutex.Unlock()
|
||||
|
||||
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// let's try to find our file again and move the cursor to that
|
||||
if selectedFile != nil {
|
||||
for idx, f := range gui.State.Files {
|
||||
selectedFileHasMoved := f.Matches(selectedFile) && idx != prevSelectedLineIdx
|
||||
if selectedFileHasMoved {
|
||||
gui.State.Panels.Files.SelectedLineIdx = idx
|
||||
break
|
||||
if selectedNode != nil {
|
||||
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileManager.GetAllItems())
|
||||
if newIdx != -1 && newIdx != prevSelectedLineIdx {
|
||||
newNode := state.FileManager.GetItemAtIndex(newIdx)
|
||||
// when not in tree mode, we show merge conflict files at the top, so you
|
||||
// can work through them one by one without having to sift through a large
|
||||
// set of files. If you have just fixed the merge conflicts of a file, we
|
||||
// actually don't want to jump to that file's new position, because that
|
||||
// file will now be ages away amidst the other files without merge
|
||||
// conflicts: the user in this case would rather work on the next file
|
||||
// with merge conflicts, which will have moved up to fill the gap left by
|
||||
// the last file, meaning the cursor doesn't need to move at all.
|
||||
leaveCursor := !state.FileManager.InTreeMode() && newNode != nil &&
|
||||
selectedNode.File != nil && selectedNode.File.HasMergeConflicts &&
|
||||
newNode.File != nil && !newNode.File.HasMergeConflicts
|
||||
|
||||
if !leaveCursor {
|
||||
state.Panels.Files.SelectedLineIdx = newIdx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
|
||||
gui.refreshSelectedLine(state.Panels.Files, state.FileManager.GetItemsLength())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// Let's try to find our file again and move the cursor to that.
|
||||
// If we can't find our file, it was probably just removed by the user. In that
|
||||
// case, we go looking for where the next file has been moved to. Given that the
|
||||
// user could have removed a whole directory, we continue iterating through the old
|
||||
// nodes until we find one that exists in the new set of nodes, then move the cursor
|
||||
// to that.
|
||||
// prevNodes starts from our previously selected node because we don't need to consider anything above that
|
||||
func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int {
|
||||
getPaths := func(node *filetree.FileNode) []string {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
return node.File.Names()
|
||||
} else {
|
||||
return []string{node.Path}
|
||||
}
|
||||
}
|
||||
|
||||
for _, prevNode := range prevNodes {
|
||||
selectedPaths := getPaths(prevNode)
|
||||
|
||||
for idx, node := range currNodes {
|
||||
paths := getPaths(node)
|
||||
|
||||
// If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file.
|
||||
// This is because the new should be in the same position as the rename was meaning less cursor jumping
|
||||
foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName
|
||||
foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename
|
||||
if foundNode {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
@@ -563,7 +694,7 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, args string) error {
|
||||
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -580,7 +711,7 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, upstream, args)
|
||||
return gui.pushWithForceFlag(true, upstream, args)
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -591,7 +722,7 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) pushFiles() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
@@ -607,23 +738,23 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
return gui.pushWithForceFlag(false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
}
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
|
||||
return gui.pushWithForceFlag(false, "", "--set-upstream")
|
||||
} else {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.pushWithForceFlag(v, false, response, "")
|
||||
return gui.pushWithForceFlag(false, response, "")
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(v, false, "", "")
|
||||
return gui.pushWithForceFlag(false, "", "")
|
||||
}
|
||||
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
@@ -635,7 +766,7 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, "", "")
|
||||
return gui.pushWithForceFlag(true, "", "")
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -650,7 +781,7 @@ func (gui *Gui) handleSwitchToMerge() error {
|
||||
return gui.createErrorPanel(gui.Tr.FileNoMergeCons)
|
||||
}
|
||||
|
||||
return gui.pushContext(gui.Contexts.Merging.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Merging)
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
@@ -661,7 +792,7 @@ func (gui *Gui) openFile(filename string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
@@ -669,17 +800,18 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
handleConfirm: func(command string) error {
|
||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateStashMenu() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcStashAllChanges,
|
||||
@@ -698,10 +830,52 @@ func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createMenu(gui.Tr.LcStashOptions, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashChanges() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateResetToUpstreamMenu() error {
|
||||
return gui.createResetMenu("@{upstream}")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleDirCollapsed() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.FileManager.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileTreeView() error {
|
||||
// get path of currently selected file
|
||||
path := gui.getSelectedPath()
|
||||
|
||||
gui.State.FileManager.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.FileManager.ExpandToPath(path)
|
||||
index, found := gui.State.FileManager.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.filesListContext().GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
}
|
||||
|
||||
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
|
||||
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.State.Contexts.Files.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
110
pkg/gui/filetree/build_tree.go
Normal file
110
pkg/gui/filetree/build_tree.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
root := &FileNode{}
|
||||
|
||||
var curr *FileNode
|
||||
for _, file := range files {
|
||||
split := strings.Split(file.Name, string(os.PathSeparator))
|
||||
curr = root
|
||||
outer:
|
||||
for i := range split {
|
||||
var setFile *models.File
|
||||
isFile := i == len(split)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := filepath.Join(split[:i+1]...)
|
||||
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
curr = existingChild
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &FileNode{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
curr.Children = append(curr.Children, newChild)
|
||||
|
||||
curr = newChild
|
||||
}
|
||||
}
|
||||
|
||||
root.Sort()
|
||||
root.Compress()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
rootAux := BuildTreeFromCommitFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
return &CommitFileNode{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
root := &CommitFileNode{}
|
||||
|
||||
var curr *CommitFileNode
|
||||
for _, file := range files {
|
||||
split := strings.Split(file.Name, string(os.PathSeparator))
|
||||
curr = root
|
||||
outer:
|
||||
for i := range split {
|
||||
var setFile *models.CommitFile
|
||||
isFile := i == len(split)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := filepath.Join(split[:i+1]...)
|
||||
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
curr = existingChild
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &CommitFileNode{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
curr.Children = append(curr.Children, newChild)
|
||||
|
||||
curr = newChild
|
||||
}
|
||||
}
|
||||
|
||||
root.Sort()
|
||||
root.Compress()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
// Move merge conflicts to top. This is the one way in which sorting
|
||||
// differs between flat mode and tree mode
|
||||
sort.SliceStable(sortedFiles, func(i, j int) bool {
|
||||
return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts)
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
}
|
||||
25
pkg/gui/filetree/collapsed_paths.go
Normal file
25
pkg/gui/filetree/collapsed_paths.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CollapsedPaths map[string]bool
|
||||
|
||||
func (cp CollapsedPaths) ExpandToPath(path string) {
|
||||
// need every directory along the way
|
||||
split := strings.Split(path, string(os.PathSeparator))
|
||||
for i := range split {
|
||||
dir := strings.Join(split[0:i+1], string(os.PathSeparator))
|
||||
cp[dir] = false
|
||||
}
|
||||
}
|
||||
|
||||
func (cp CollapsedPaths) IsCollapsed(path string) bool {
|
||||
return cp[path]
|
||||
}
|
||||
|
||||
func (cp CollapsedPaths) ToggleCollapsed(path string) {
|
||||
cp[path] = !cp[path]
|
||||
}
|
||||
119
pkg/gui/filetree/commit_file_manager.go
Normal file
119
pkg/gui/filetree/commit_file_manager.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CommitFileManager struct {
|
||||
files []*models.CommitFile
|
||||
tree *CommitFileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths CollapsedPaths
|
||||
// parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
|
||||
parent string
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetParent() string {
|
||||
return m.parent
|
||||
}
|
||||
|
||||
func NewCommitFileManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileManager {
|
||||
return &CommitFileManager{
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetItemAtIndex(index int) *CommitFileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetAllItems() []*CommitFileNode {
|
||||
if m.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetItemsLength() int {
|
||||
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetAllFiles() []*models.CommitFile {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) SetFiles(files []*models.CommitFile, parent string) {
|
||||
m.files = files
|
||||
m.parent = parent
|
||||
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) SetTree() {
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromCommitFiles(m.files)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromCommitFiles(m.files)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) IsCollapsed(path string) bool {
|
||||
return m.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ToggleCollapsed(path string) {
|
||||
m.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchManager) []string {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*CommitFileNode)
|
||||
|
||||
// This is a little convoluted because we're dealing with either a leaf or a non-leaf.
|
||||
// But this code actually applies to both. If it's a leaf, the status will just
|
||||
// be whatever status it is, but if it's a non-leaf it will determine its status
|
||||
// based on the leaves of that subtree
|
||||
var status patch.PatchStatus
|
||||
if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE
|
||||
}) {
|
||||
status = patch.WHOLE
|
||||
} else if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED
|
||||
}) {
|
||||
status = patch.UNSELECTED
|
||||
} else {
|
||||
status = patch.PART
|
||||
}
|
||||
|
||||
return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
})
|
||||
}
|
||||
176
pkg/gui/filetree/commit_file_node.go
Normal file
176
pkg/gui/filetree/commit_file_node.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type CommitFileNode struct {
|
||||
Children []*CommitFileNode
|
||||
File *models.CommitFile
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *CommitFileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *CommitFileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetChildren() []INode {
|
||||
result := make([]INode, len(s.Children))
|
||||
for i, child := range s.Children {
|
||||
result[i] = child
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetChildren(children []INode) {
|
||||
castChildren := make([]*CommitFileNode, len(children))
|
||||
for i, child := range children {
|
||||
castChildren[i] = child.(*CommitFileNode)
|
||||
}
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *CommitFileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
|
||||
return castNode.File == nil || test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
nodes := make([]*CommitFileNode, len(results))
|
||||
for i, result := range results {
|
||||
nodes[i] = result.(*CommitFileNode)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*CommitFileNode)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) Size(collapsedPaths map[string]bool) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
// This ignores the root
|
||||
func (node *CommitFileNode) GetPathsMatching(test func(*CommitFileNode) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
return test(n.(*CommitFileNode))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetLeaves() []*CommitFileNode {
|
||||
leaves := getLeaves(s)
|
||||
castLeaves := make([]*CommitFileNode, len(leaves))
|
||||
for i := range leaves {
|
||||
castLeaves[i] = leaves[i].(*CommitFileNode)
|
||||
}
|
||||
|
||||
return castLeaves
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
|
||||
return s.Any(func(node *CommitFileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) NameAtDepth(depth int) string {
|
||||
splitName := strings.Split(s.Path, string(os.PathSeparator))
|
||||
name := filepath.Join(splitName[depth:]...)
|
||||
|
||||
return name
|
||||
}
|
||||
9
pkg/gui/filetree/constants.go
Normal file
9
pkg/gui/filetree/constants.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package filetree
|
||||
|
||||
const EXPANDED_ARROW = "▼"
|
||||
const COLLAPSED_ARROW = "►"
|
||||
|
||||
const INNER_ITEM = "├─ "
|
||||
const LAST_ITEM = "└─ "
|
||||
const NESTED = "│ "
|
||||
const NOTHING = " "
|
||||
101
pkg/gui/filetree/file_manager.go
Normal file
101
pkg/gui/filetree/file_manager.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileManager struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager {
|
||||
return &FileManager{
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileManager) InTreeMode() bool {
|
||||
return m.showTree
|
||||
}
|
||||
|
||||
func (m *FileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) GetItemAtIndex(index int) *FileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (m *FileManager) GetAllItems() []*FileNode {
|
||||
if m.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetItemsLength() int {
|
||||
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetAllFiles() []*models.File {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *FileManager) SetFiles(files []*models.File) {
|
||||
m.files = files
|
||||
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) SetTree() {
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromFiles(m.files)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromFiles(m.files)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileManager) IsCollapsed(path string) bool {
|
||||
return m.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleCollapsed(path string) {
|
||||
m.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*FileNode)
|
||||
return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
})
|
||||
}
|
||||
91
pkg/gui/filetree/file_manager_test.go
Normal file
91
pkg/gui/filetree/file_manager_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
collapsedPaths map[string]bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
root: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: []string{" M test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir2/dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file5",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"},
|
||||
collapsedPaths: map[string]bool{"dir1": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileManager{tree: s.root, collapsedPaths: s.collapsedPaths}
|
||||
result := mngr.Render("", nil)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
192
pkg/gui/filetree/file_node.go
Normal file
192
pkg/gui/filetree/file_node.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type FileNode struct {
|
||||
Children []*FileNode
|
||||
File *models.File
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *FileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *FileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *FileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *FileNode) GetChildren() []INode {
|
||||
result := make([]INode, len(s.Children))
|
||||
for i, child := range s.Children {
|
||||
result[i] = child
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *FileNode) SetChildren(children []INode) {
|
||||
castChildren := make([]*FileNode, len(children))
|
||||
for i, child := range children {
|
||||
castChildren[i] = child.(*FileNode)
|
||||
}
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *FileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *FileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *FileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *FileNode) ForEachFile(cb func(*models.File) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*FileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) Any(test func(node *FileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
nodes := make([]*FileNode, len(results))
|
||||
for i, result := range results {
|
||||
nodes[i] = result.(*FileNode)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*FileNode)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *FileNode) Size(collapsedPaths map[string]bool) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *FileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
// This ignores the root
|
||||
func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
return test(n.(*FileNode))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) GetLeaves() []*FileNode {
|
||||
leaves := getLeaves(s)
|
||||
castLeaves := make([]*FileNode, len(leaves))
|
||||
for i := range leaves {
|
||||
castLeaves[i] = leaves[i].(*FileNode)
|
||||
}
|
||||
|
||||
return castLeaves
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *FileNode) GetHasUnstagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasStagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetIsTracked() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
|
||||
return s.Any(func(node *FileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) NameAtDepth(depth int) string {
|
||||
splitName := strings.Split(s.Path, string(os.PathSeparator))
|
||||
name := filepath.Join(splitName[depth:]...)
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator))
|
||||
|
||||
prevName := s.File.PreviousName
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...)
|
||||
if sameParentDir {
|
||||
prevName = filepath.Join(splitPrevName[depth:]...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
125
pkg/gui/filetree/file_node_test.go
Normal file
125
pkg/gui/filetree/file_node_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
root: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir3",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1/file2",
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir3/dir3-1/file5",
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 2,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
s.root.Compress()
|
||||
assert.EqualValues(t, s.expected, s.root)
|
||||
})
|
||||
}
|
||||
}
|
||||
262
pkg/gui/filetree/inode.go
Normal file
262
pkg/gui/filetree/inode.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type INode interface {
|
||||
IsLeaf() bool
|
||||
GetPath() string
|
||||
GetChildren() []INode
|
||||
SetChildren([]INode)
|
||||
GetCompressionLevel() int
|
||||
SetCompressionLevel(int)
|
||||
}
|
||||
|
||||
func sortNode(node INode) {
|
||||
sortChildren(node)
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
sortNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
func sortChildren(node INode) {
|
||||
if node.IsLeaf() {
|
||||
return
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
sortedChildren := make([]INode, len(children))
|
||||
copy(sortedChildren, children)
|
||||
|
||||
sort.Slice(sortedChildren, func(i, j int) bool {
|
||||
if !sortedChildren[i].IsLeaf() && sortedChildren[j].IsLeaf() {
|
||||
return true
|
||||
}
|
||||
if sortedChildren[i].IsLeaf() && !sortedChildren[j].IsLeaf() {
|
||||
return false
|
||||
}
|
||||
|
||||
return sortedChildren[i].GetPath() < sortedChildren[j].GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
node.SetChildren(sortedChildren)
|
||||
}
|
||||
|
||||
func forEachLeaf(node INode, cb func(INode) error) error {
|
||||
if node.IsLeaf() {
|
||||
if err := cb(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if err := forEachLeaf(child, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func any(node INode, test func(INode) bool) bool {
|
||||
if test(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if any(child, test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func every(node INode, test func(INode) bool) bool {
|
||||
if !test(node) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if !every(child, test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func flatten(node INode, collapsedPaths map[string]bool) []INode {
|
||||
result := []INode{}
|
||||
result = append(result, node)
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
result = append(result, flatten(child, collapsedPaths)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getNodeAtIndex(node INode, index int, collapsedPaths map[string]bool) INode {
|
||||
foundNode, _ := getNodeAtIndexAux(node, index, collapsedPaths)
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
func getNodeAtIndexAux(node INode, index int, collapsedPaths map[string]bool) (INode, int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return node, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
foundNode, offsetChange := getNodeAtIndexAux(child, index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func getIndexForPath(node INode, path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if node.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
offsetChange, found := getIndexForPath(child, path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func size(node INode, collapsedPaths map[string]bool) int {
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
output += size(child, collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func compressAux(node INode) INode {
|
||||
if node.IsLeaf() {
|
||||
return node
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
for i := range children {
|
||||
grandchildren := children[i].GetChildren()
|
||||
for len(grandchildren) == 1 {
|
||||
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].GetChildren()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = compressAux(children[i])
|
||||
}
|
||||
|
||||
node.SetChildren(children)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func getPathsMatching(node INode, test func(INode) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(node) {
|
||||
paths = append(paths, node.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
paths = append(paths, getPathsMatching(child, test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func getLeaves(node INode) []INode {
|
||||
if node.IsLeaf() {
|
||||
return []INode{node}
|
||||
}
|
||||
|
||||
output := []INode{}
|
||||
for _, child := range node.GetChildren() {
|
||||
output = append(output, getLeaves(child)...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func renderAux(s INode, collapsedPaths CollapsedPaths, prefix string, depth int, renderLine func(INode, int) string) []string {
|
||||
isRoot := depth == -1
|
||||
|
||||
renderLineWithPrefix := func() string {
|
||||
return prefix + renderLine(s, depth)
|
||||
}
|
||||
|
||||
if s.IsLeaf() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{renderLineWithPrefix()}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ARROW))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
if strings.HasSuffix(prefix, LAST_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
|
||||
} else if strings.HasSuffix(prefix, INNER_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
childPrefix = newPrefix
|
||||
} else if isLast {
|
||||
childPrefix = newPrefix + LAST_ITEM
|
||||
} else {
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
@@ -14,6 +14,29 @@ func (gui *Gui) validateNotInFilterMode() (bool, error) {
|
||||
}
|
||||
|
||||
func (gui *Gui) exitFilterMode() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.clearFiltering()
|
||||
}
|
||||
|
||||
func (gui *Gui) clearFiltering() error {
|
||||
gui.State.Modes.Filtering.Reset()
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
gui.State.ScreenMode = SCREEN_NORMAL
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}})
|
||||
}
|
||||
|
||||
func (gui *Gui) setFiltering(path string) error {
|
||||
gui.State.Modes.Filtering.SetPath(path)
|
||||
if gui.State.ScreenMode == SCREEN_NORMAL {
|
||||
gui.State.ScreenMode = SCREEN_HALF
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}, then: func() {
|
||||
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
|
||||
}})
|
||||
}
|
||||
|
||||
@@ -3,26 +3,20 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFilteringMenuPanel() error {
|
||||
fileName := ""
|
||||
switch v.Name() {
|
||||
case "files":
|
||||
file := gui.getSelectedFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
switch gui.currentSideListContext() {
|
||||
case gui.State.Contexts.Files:
|
||||
node := gui.getSelectedFileNode()
|
||||
if node != nil {
|
||||
fileName = node.GetPath()
|
||||
}
|
||||
case "commitFiles":
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
case gui.State.Contexts.CommitFiles:
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node != nil {
|
||||
fileName = node.GetPath()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +26,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = fileName
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.setFiltering(fileName)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -44,8 +37,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnterFileName,
|
||||
handleConfirm: func(response string) error {
|
||||
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.setFiltering(strings.TrimSpace(response))
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -54,10 +46,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcExitFilterMode,
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
onPress: gui.clearFiltering,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -32,19 +31,19 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
return gui.createErrorPanel(gui.Tr.NotAGitFlowBranch)
|
||||
}
|
||||
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get config
|
||||
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
if err != nil {
|
||||
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
|
||||
}
|
||||
@@ -56,9 +55,9 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
handleConfirm: func(name string) error {
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
// 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 {
|
||||
for _, viewName := range []string{"branches", "commits"} {
|
||||
if err := gui.rerenderView(viewName); err != nil {
|
||||
for _, view := range []*gocui.View{gui.Views.Branches, gui.Views.Commits} {
|
||||
if err := gui.rerenderView(view); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -22,131 +22,167 @@ func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpView(viewName string) error {
|
||||
mainView, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
// TODO: GENERICS
|
||||
func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
|
||||
for i, val := range sl {
|
||||
if val == current {
|
||||
if i == len(sl)-1 {
|
||||
return sl[0]
|
||||
}
|
||||
return sl[i+1]
|
||||
}
|
||||
}
|
||||
ox, oy := mainView.Origin()
|
||||
return sl[0]
|
||||
}
|
||||
|
||||
// TODO: GENERICS
|
||||
func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
|
||||
for i, val := range sl {
|
||||
if val == current {
|
||||
if i > 0 {
|
||||
return sl[i-1]
|
||||
}
|
||||
return sl[len(sl)-1]
|
||||
}
|
||||
}
|
||||
return sl[len(sl)-1]
|
||||
}
|
||||
|
||||
func (gui *Gui) nextScreenMode() error {
|
||||
gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) prevScreenMode() error {
|
||||
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
|
||||
return mainView.SetOrigin(ox, newOy)
|
||||
return view.SetOrigin(ox, newOy)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownView(viewName string) error {
|
||||
mainView, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ox, oy := mainView.Origin()
|
||||
func (gui *Gui) scrollDownView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
y := oy
|
||||
if !gui.Config.GetUserConfig().Gui.ScrollPastBottom {
|
||||
_, sy := mainView.Size()
|
||||
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
|
||||
if !canScrollPastBottom {
|
||||
_, sy := view.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
|
||||
if y < mainView.LinesHeight() {
|
||||
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
scrollableLines := view.ViewLinesHeight() - y
|
||||
if scrollableLines > 0 {
|
||||
// margin is about how many lines must still appear if you scroll
|
||||
// all the way down. In practice every file ends in a newline so it will really
|
||||
// just show a single line
|
||||
margin := 1
|
||||
if canScrollPastBottom {
|
||||
margin = 2
|
||||
}
|
||||
if scrollableLines-margin < scrollHeight {
|
||||
scrollHeight = scrollableLines - margin
|
||||
}
|
||||
if oy+scrollHeight >= 0 {
|
||||
if err := view.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
|
||||
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView("main")
|
||||
return gui.scrollUpView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView("main")
|
||||
return gui.scrollDownView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollUpView("secondary")
|
||||
func (gui *Gui) scrollUpSecondary() error {
|
||||
return gui.scrollUpView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollDownView("secondary")
|
||||
func (gui *Gui) scrollDownSecondary() error {
|
||||
return gui.scrollDownView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
func (gui *Gui) scrollUpConfirmationPanel() error {
|
||||
if gui.Views.Confirmation.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollUpView("confirmation")
|
||||
|
||||
return gui.scrollUpView(gui.Views.Confirmation)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
func (gui *Gui) scrollDownConfirmationPanel() error {
|
||||
if gui.Views.Confirmation.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollDownView("confirmation")
|
||||
|
||||
return gui.scrollDownView(gui.Views.Confirmation)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRefresh() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMouseDownMain() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
switch gui.g.CurrentView() {
|
||||
case gui.Views.Files:
|
||||
// set filename, set primary/secondary selected, set line number, then switch context
|
||||
// I'll need to know it was changed though.
|
||||
// Could I pass something along to the context change?
|
||||
return gui.enterFile(false, v.SelectedLineIdx())
|
||||
case "commitFiles":
|
||||
return gui.enterCommitFile(v.SelectedLineIdx())
|
||||
return gui.enterFile(false, gui.Views.Main.SelectedLineIdx())
|
||||
case gui.Views.CommitFiles:
|
||||
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMouseDownSecondary() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
return gui.enterFile(true, v.SelectedLineIdx())
|
||||
switch gui.g.CurrentView() {
|
||||
case gui.Views.Files:
|
||||
return gui.enterFile(true, gui.Views.Secondary.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleInfoClick() error {
|
||||
if !gui.g.Mouse {
|
||||
return nil
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
width, _ := v.Size()
|
||||
view := gui.Views.Information
|
||||
|
||||
cx, _ := view.Cursor()
|
||||
width, _ := view.Size()
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
@@ -179,7 +215,7 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
|
||||
}
|
||||
|
||||
_ = gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
_ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
389
pkg/gui/gui.go
389
pkg/gui/gui.go
@@ -3,6 +3,7 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
@@ -11,8 +12,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -21,19 +20,26 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/termbox-go"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// screen sizing determines how much space your selected window takes up (window
|
||||
// as in panel, not your terminal's window). Sometimes you want a bit more space
|
||||
// to see the contents of a panel, and this keeps track of how much maximisation
|
||||
// you've set
|
||||
type WindowMaximisation int
|
||||
|
||||
const (
|
||||
SCREEN_NORMAL int = iota
|
||||
SCREEN_NORMAL WindowMaximisation = iota
|
||||
SCREEN_HALF
|
||||
SCREEN_FULL
|
||||
)
|
||||
@@ -43,56 +49,35 @@ const StartupPopupVersion = 3
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
// SentinelErrors are the errors that have special meaning and need to be checked
|
||||
// by calling functions. The less of these, the better
|
||||
type SentinelErrors struct {
|
||||
ErrSubProcess error
|
||||
ErrNoFiles error
|
||||
ErrSwitchRepo error
|
||||
ErrRestart error
|
||||
type ContextManager struct {
|
||||
ContextStack []Context
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
|
||||
|
||||
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
|
||||
// because we can't do package-scoped errors with localization, and also because
|
||||
// it seems like package-scoped variables are bad in general
|
||||
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
|
||||
// In the future it would be good to implement some of the recommendations of
|
||||
// that article. For now, if we don't need an error to be a sentinel, we will just
|
||||
// define it inline. This has implications for error messages that pop up everywhere
|
||||
// in that we'll be duplicating the default values. We may need to look at
|
||||
// having a default localisation bundle defined, and just using keys-only when
|
||||
// localising things in the code.
|
||||
func (gui *Gui) GenerateSentinelErrors() {
|
||||
gui.Errors = SentinelErrors{
|
||||
ErrSubProcess: errors.New(gui.Tr.RunningSubprocess),
|
||||
ErrNoFiles: errors.New(gui.Tr.NoChangedFiles),
|
||||
ErrSwitchRepo: errors.New("switching repo"),
|
||||
ErrRestart: errors.New("restarting"),
|
||||
func NewContextManager(initialContext Context) ContextManager {
|
||||
return ContextManager{
|
||||
ContextStack: []Context{initialContext},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) sentinelErrorsArr() []error {
|
||||
return []error{
|
||||
gui.Errors.ErrSubProcess,
|
||||
gui.Errors.ErrNoFiles,
|
||||
gui.Errors.ErrSwitchRepo,
|
||||
gui.Errors.ErrRestart,
|
||||
}
|
||||
}
|
||||
type Repo string
|
||||
|
||||
// Gui wraps the gocui Gui object which handles rendering and events
|
||||
type Gui struct {
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *oscommands.OSCommand
|
||||
SubProcess *exec.Cmd
|
||||
State *guiState
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *oscommands.OSCommand
|
||||
|
||||
// this is the state of the GUI for the current repo
|
||||
State *guiState
|
||||
|
||||
// this is a mapping of repos to gui states, so that we can restore the original
|
||||
// gui state when returning from a subrepo
|
||||
RepoStateMap map[Repo]*guiState
|
||||
Config config.AppConfigurer
|
||||
Tr *i18n.TranslationSet
|
||||
Errors SentinelErrors
|
||||
Updater *updates.Updater
|
||||
statusManager *statusManager
|
||||
credentials credentials
|
||||
@@ -103,25 +88,22 @@ type Gui struct {
|
||||
|
||||
// when lazygit is opened outside a git directory we want to open to the most
|
||||
// recent repo with the recent repos popup showing
|
||||
showRecentRepos bool
|
||||
Contexts ContextTree
|
||||
ViewTabContextMap map[string][]tabContext
|
||||
|
||||
// this array either includes the events that we're recording in this session
|
||||
// or the events we've recorded in a prior session
|
||||
RecordedEvents []RecordedEvent
|
||||
StartTime time.Time
|
||||
showRecentRepos bool
|
||||
|
||||
Mutexes guiStateMutexes
|
||||
|
||||
// findSuggestions will take a string that the user has typed into a prompt
|
||||
// and return a slice of suggestions which match that string.
|
||||
findSuggestions func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
type RecordedEvent struct {
|
||||
Timestamp int64
|
||||
Event *termbox.Event
|
||||
// when you enter into a submodule we'll append the superproject's path to this array
|
||||
// so that you can return to the superproject
|
||||
RepoPathStack []string
|
||||
|
||||
// this tells us whether our views have been initially set up
|
||||
ViewsSetup bool
|
||||
|
||||
Views Views
|
||||
}
|
||||
|
||||
type listPanelState struct {
|
||||
@@ -136,11 +118,6 @@ func (h *listPanelState) GetSelectedLineIdx() int {
|
||||
return h.SelectedLineIdx
|
||||
}
|
||||
|
||||
type IListPanelState interface {
|
||||
SetSelectedLineIdx(int)
|
||||
GetSelectedLineIdx() int
|
||||
}
|
||||
|
||||
// for now the staging panel state, unlike the other panel states, is going to be
|
||||
// non-mutative, so that we don't accidentally end up
|
||||
// with mismatches of data. We might change this in the future
|
||||
@@ -150,15 +127,16 @@ type lBlPanelState struct {
|
||||
LastLineIdx int
|
||||
Diff string
|
||||
PatchParser *patch.PatchParser
|
||||
SelectMode int // one of LINE, HUNK, or RANGE
|
||||
SelectMode SelectMode
|
||||
SecondaryFocused bool // this is for if we show the left or right panel
|
||||
}
|
||||
|
||||
type mergingPanelState struct {
|
||||
ConflictIndex int
|
||||
ConflictTop bool
|
||||
Conflicts []commands.Conflict
|
||||
EditHistory *stack.Stack
|
||||
ConflictIndex int
|
||||
ConflictTop bool
|
||||
Conflicts []commands.Conflict
|
||||
ConflictsMutex sync.Mutex
|
||||
EditHistory *stack.Stack
|
||||
|
||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||
// in which case we won't auto-scroll to a conflict.
|
||||
@@ -247,6 +225,28 @@ type panelStates struct {
|
||||
Suggestions *suggestionsPanelState
|
||||
}
|
||||
|
||||
type Views struct {
|
||||
Status *gocui.View
|
||||
Files *gocui.View
|
||||
Branches *gocui.View
|
||||
Commits *gocui.View
|
||||
Stash *gocui.View
|
||||
Main *gocui.View
|
||||
Secondary *gocui.View
|
||||
Options *gocui.View
|
||||
Confirmation *gocui.View
|
||||
Menu *gocui.View
|
||||
Credentials *gocui.View
|
||||
CommitMessage *gocui.View
|
||||
CommitFiles *gocui.View
|
||||
Information *gocui.View
|
||||
AppStatus *gocui.View
|
||||
Search *gocui.View
|
||||
SearchPrefix *gocui.View
|
||||
Limit *gocui.View
|
||||
Suggestions *gocui.View
|
||||
}
|
||||
|
||||
type searchingState struct {
|
||||
view *gocui.View
|
||||
isSearching bool
|
||||
@@ -254,8 +254,10 @@ type searchingState struct {
|
||||
}
|
||||
|
||||
// startup stages so we don't need to load everything at once
|
||||
type StartupStage int
|
||||
|
||||
const (
|
||||
INITIAL = iota
|
||||
INITIAL StartupStage = iota
|
||||
COMPLETE
|
||||
)
|
||||
|
||||
@@ -269,19 +271,11 @@ func (m *Diffing) Active() bool {
|
||||
return m.Ref != ""
|
||||
}
|
||||
|
||||
type Filtering struct {
|
||||
Path string // the filename that gets passed to git log
|
||||
}
|
||||
|
||||
func (m *Filtering) Active() bool {
|
||||
return m.Path != ""
|
||||
}
|
||||
|
||||
type CherryPicking struct {
|
||||
CherryPickedCommits []*models.Commit
|
||||
|
||||
// we only allow cherry picking from one context at a time, so you can't copy a commit from the local commits context and then also copy a commit in the reflog context
|
||||
ContextKey string
|
||||
ContextKey ContextKey
|
||||
}
|
||||
|
||||
func (m *CherryPicking) Active() bool {
|
||||
@@ -289,7 +283,7 @@ func (m *CherryPicking) Active() bool {
|
||||
}
|
||||
|
||||
type Modes struct {
|
||||
Filtering Filtering
|
||||
Filtering filtering.Filtering
|
||||
CherryPicking CherryPicking
|
||||
Diffing Diffing
|
||||
}
|
||||
@@ -303,12 +297,14 @@ type guiStateMutexes struct {
|
||||
}
|
||||
|
||||
type guiState struct {
|
||||
Files []*models.File
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
CommitFiles []*models.CommitFile
|
||||
// the file panels (files and commit files) can render as a tree, so we have
|
||||
// managers for them which handle rendering a flat list of files in tree form
|
||||
FileManager *filetree.FileManager
|
||||
CommitFileManager *filetree.CommitFileManager
|
||||
Submodules []*models.SubmoduleConfig
|
||||
Branches []*models.Branch
|
||||
Commits []*models.Commit
|
||||
StashEntries []*models.StashEntry
|
||||
// Suggestions will sometimes appear when typing into a prompt
|
||||
Suggestions []*types.Suggestion
|
||||
// FilteredReflogCommits are the ones that appear in the reflog panel.
|
||||
@@ -325,60 +321,75 @@ type guiState struct {
|
||||
MenuItems []*menuItem
|
||||
Updating bool
|
||||
Panels *panelStates
|
||||
MainContext string // used to keep the main and secondary views' contexts in sync
|
||||
SplitMainPanel bool
|
||||
MainContext ContextKey // used to keep the main and secondary views' contexts in sync
|
||||
RetainOriginalDir bool
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
ScreenMode int
|
||||
ScreenMode WindowMaximisation
|
||||
SideView *gocui.View
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
PrevMainHeight int
|
||||
OldInformation string
|
||||
StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once
|
||||
StartupStage StartupStage // Allows us to not load everything at once
|
||||
|
||||
Modes Modes
|
||||
|
||||
ContextStack []Context
|
||||
ViewContextMap map[string]Context
|
||||
ContextManager ContextManager
|
||||
Contexts ContextTree
|
||||
ViewContextMap map[string]Context
|
||||
ViewTabContextMap map[string][]tabContext
|
||||
|
||||
// WindowViewNameMap is a mapping of windows to the current view of that window.
|
||||
// Some views move between windows for example the commitFiles view and when cycling through
|
||||
// side windows we need to know which view to give focus to for a given window
|
||||
WindowViewNameMap map[string]string
|
||||
|
||||
// when you enter into a submodule we'll append the superproject's path to this array
|
||||
// so that you can return to the superproject
|
||||
RepoPathStack []string
|
||||
// tells us whether we've set up our views for the current repo. We'll need to
|
||||
// do this whenever we switch back and forth between repos to get the views
|
||||
// back in sync with the repo state
|
||||
ViewsSetup bool
|
||||
}
|
||||
|
||||
func (gui *Gui) resetState() {
|
||||
// we carry over the filter path and diff state
|
||||
prevFiltering := Filtering{
|
||||
Path: "",
|
||||
}
|
||||
prevDiff := Diffing{}
|
||||
prevCherryPicking := CherryPicking{
|
||||
CherryPickedCommits: make([]*models.Commit, 0),
|
||||
ContextKey: "",
|
||||
}
|
||||
prevRepoPathStack := []string{}
|
||||
if gui.State != nil {
|
||||
prevFiltering = gui.State.Modes.Filtering
|
||||
prevDiff = gui.State.Modes.Diffing
|
||||
prevCherryPicking = gui.State.Modes.CherryPicking
|
||||
prevRepoPathStack = gui.State.RepoPathStack
|
||||
// reuseState determines if we pull the repo state from our repo state map or
|
||||
// just re-initialize it. For now we're only re-using state when we're going
|
||||
// in and out of submodules, for the sake of having the cursor back on the submodule
|
||||
// when we return.
|
||||
//
|
||||
// I tried out always reverting to the repo's original state but found that in fact
|
||||
// it gets a bit confusing to land back in the status panel when visiting a repo
|
||||
// you've already switched from. There's no doubt some easy way to make the UX
|
||||
// optimal for all cases but I'm too lazy to think about what that is right now
|
||||
func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
currentDir, err := os.Getwd()
|
||||
|
||||
if reuseState {
|
||||
if err == nil {
|
||||
if state := gui.RepoStateMap[Repo(currentDir)]; state != nil {
|
||||
gui.State = state
|
||||
gui.State.ViewsSetup = false
|
||||
return
|
||||
}
|
||||
} else {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
modes := Modes{
|
||||
Filtering: prevFiltering,
|
||||
CherryPicking: prevCherryPicking,
|
||||
Diffing: prevDiff,
|
||||
showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
|
||||
|
||||
contexts := gui.contextTree()
|
||||
|
||||
screenMode := SCREEN_NORMAL
|
||||
initialContext := contexts.Files
|
||||
if filterPath != "" {
|
||||
screenMode = SCREEN_HALF
|
||||
initialContext = contexts.BranchCommits
|
||||
}
|
||||
|
||||
gui.State = &guiState{
|
||||
Files: make([]*models.File, 0),
|
||||
FileManager: filetree.NewFileManager(make([]*models.File, 0), gui.Log, showTree),
|
||||
CommitFileManager: filetree.NewCommitFileManager(make([]*models.CommitFile, 0), gui.Log, showTree),
|
||||
Commits: make([]*models.Commit, 0),
|
||||
FilteredReflogCommits: make([]*models.Commit, 0),
|
||||
ReflogCommits: make([]*models.Commit, 0),
|
||||
@@ -399,18 +410,32 @@ func (gui *Gui) resetState() {
|
||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||
Merging: &mergingPanelState{
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: []commands.Conflict{},
|
||||
EditHistory: stack.New(),
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: []commands.Conflict{},
|
||||
EditHistory: stack.New(),
|
||||
ConflictsMutex: sync.Mutex{},
|
||||
},
|
||||
},
|
||||
SideView: nil,
|
||||
Ptmx: nil,
|
||||
Modes: modes,
|
||||
ViewContextMap: gui.initialViewContextMap(),
|
||||
RepoPathStack: prevRepoPathStack,
|
||||
SideView: nil,
|
||||
Ptmx: nil,
|
||||
Modes: Modes{
|
||||
Filtering: filtering.NewFiltering(filterPath),
|
||||
CherryPicking: CherryPicking{
|
||||
CherryPickedCommits: make([]*models.Commit, 0),
|
||||
ContextKey: "",
|
||||
},
|
||||
Diffing: Diffing{},
|
||||
},
|
||||
ViewContextMap: contexts.initialViewContextMap(),
|
||||
ViewTabContextMap: contexts.initialViewTabContextMap(),
|
||||
ScreenMode: screenMode,
|
||||
// TODO: put contexts in the context manager
|
||||
ContextManager: NewContextManager(initialContext),
|
||||
Contexts: contexts,
|
||||
}
|
||||
|
||||
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
||||
}
|
||||
|
||||
// for now the split view will always be on
|
||||
@@ -426,44 +451,55 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RecordedEvents: []RecordedEvent{},
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
}
|
||||
|
||||
gui.resetState()
|
||||
gui.State.Modes.Filtering.Path = filterPath
|
||||
gui.Contexts = gui.contextTree()
|
||||
gui.ViewTabContextMap = gui.viewTabContextMap()
|
||||
gui.resetState(filterPath, false)
|
||||
|
||||
gui.watchFilesForChanges()
|
||||
|
||||
gui.GenerateSentinelErrors()
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
gui.resetState()
|
||||
|
||||
recordEvents := recordingEvents()
|
||||
playMode := gocui.NORMAL
|
||||
if recordEvents {
|
||||
playMode = gocui.RECORDING
|
||||
} else if replaying() {
|
||||
playMode = gocui.REPLAYING
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.Output256, OverlappingEdges, recordEvents)
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
defer g.Close()
|
||||
|
||||
if recordEvents {
|
||||
go utils.Safe(gui.recordEvents)
|
||||
}
|
||||
if replaying() {
|
||||
g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: getRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
}
|
||||
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
gui.State.ScreenMode = SCREEN_HALF
|
||||
} else {
|
||||
gui.State.ScreenMode = SCREEN_NORMAL
|
||||
g.Recording, err = gui.loadRecording()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
|
||||
g.OnSearchEscape = gui.onSearchEscape
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
return nil
|
||||
}
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
|
||||
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
|
||||
@@ -475,8 +511,6 @@ func (gui *Gui) Run() error {
|
||||
g.Mouse = true
|
||||
}
|
||||
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
|
||||
if err := gui.setColorScheme(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -505,20 +539,14 @@ func (gui *Gui) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
|
||||
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
|
||||
// otherwise it handles the error, possibly by quitting the application
|
||||
func (gui *Gui) RunWithSubprocesses() error {
|
||||
gui.StartTime = time.Now()
|
||||
go utils.Safe(gui.replayRecordedEvents)
|
||||
|
||||
for {
|
||||
gui.stopChan = make(chan struct{})
|
||||
// RunAndHandleError
|
||||
func (gui *Gui) RunAndHandleError() error {
|
||||
gui.stopChan = make(chan struct{})
|
||||
return utils.SafeWithError(func() error {
|
||||
if err := gui.Run(); err != nil {
|
||||
for _, manager := range gui.viewBufferManagerMap {
|
||||
manager.Close()
|
||||
}
|
||||
gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{}
|
||||
|
||||
if !gui.fileWatcher.Disabled {
|
||||
gui.fileWatcher.Watcher.Close()
|
||||
@@ -534,42 +562,63 @@ func (gui *Gui) RunWithSubprocesses() error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.saveRecordedEvents(); err != nil {
|
||||
if err := gui.saveRecording(gui.g.Recording); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
case gui.Errors.ErrSwitchRepo, gui.Errors.ErrRestart:
|
||||
continue
|
||||
case gui.Errors.ErrSubProcess:
|
||||
|
||||
if err := gui.runCommand(); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) runCommand() error {
|
||||
gui.SubProcess.Stdout = os.Stdout
|
||||
gui.SubProcess.Stderr = os.Stdout
|
||||
gui.SubProcess.Stdin = os.Stdin
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error {
|
||||
if replaying() {
|
||||
// we do not yet support running subprocesses within integration tests. So if
|
||||
// we're replaying an integration test and we're inside this method, something
|
||||
// has gone wrong, so we should fail
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue))
|
||||
log.Fatal("opening subprocesses not yet supported in integration tests. Chances are that this test is running too fast and a subprocess is accidentally opened")
|
||||
}
|
||||
|
||||
if err := gui.SubProcess.Run(); err != nil {
|
||||
if err := gocui.Screen.Suspend(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
cmdErr := gui.runSubprocess(subprocess)
|
||||
|
||||
if err := gocui.Screen.Resume(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.surfaceError(cmdErr)
|
||||
}
|
||||
|
||||
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
|
||||
subprocess.Stdout = os.Stdout
|
||||
subprocess.Stderr = os.Stdout
|
||||
subprocess.Stdin = os.Stdin
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(subprocess.Args, " "), color.FgBlue))
|
||||
|
||||
if err := subprocess.Run(); err != nil {
|
||||
// not handling the error explicitly because usually we're going to see it
|
||||
// in the output anyway
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
gui.SubProcess.Stdout = ioutil.Discard
|
||||
gui.SubProcess.Stderr = ioutil.Discard
|
||||
gui.SubProcess.Stdin = nil
|
||||
gui.SubProcess = nil
|
||||
subprocess.Stdout = ioutil.Discard
|
||||
subprocess.Stderr = ioutil.Discard
|
||||
subprocess.Stdin = nil
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
|
||||
fmt.Scanln() // wait for enter press
|
||||
@@ -578,11 +627,9 @@ func (gui *Gui) runCommand() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) loadNewRepo() error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
|
||||
if err := gui.updateRecentRepoList(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
@@ -668,6 +715,8 @@ func (gui *Gui) setColorScheme() error {
|
||||
|
||||
gui.g.FgColor = theme.InactiveBorderColor
|
||||
gui.g.SelFgColor = theme.ActiveBorderColor
|
||||
gui.g.FrameColor = theme.InactiveBorderColor
|
||||
gui.g.SelFrameColor = theme.ActiveBorderColor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,370 +1,80 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// To run an integration test, e.g. for test 'commit', go:
|
||||
// go test pkg/gui/gui_test.go -run /commit
|
||||
// This file is quite similar to integration/main.go. The main difference is that this file is
|
||||
// run via `go test` whereas the other is run via `test/lazyintegration/main.go` which provides
|
||||
// a convenient gui wrapper around our integration tests. The `go test` approach is better
|
||||
// for CI and for running locally in the background to ensure you haven't broken
|
||||
// anything while making changes. If you want to visually see what's happening when a test is run,
|
||||
// you'll need to take the other approach
|
||||
//
|
||||
// To record keypresses for an integration test, pass RECORD_EVENTS=true like so:
|
||||
// RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /commit
|
||||
// As for this file, to run an integration test, e.g. for test 'commit', go:
|
||||
// go test pkg/gui/gui_test.go -run /commit
|
||||
//
|
||||
// To update a snapshot for an integration test, pass UPDATE_SNAPSHOTS=true
|
||||
// UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /commit
|
||||
//
|
||||
// When RECORD_EVENTS is true, updates will be updated automatically
|
||||
//
|
||||
// integration tests are run in test/integration_test and the final test does
|
||||
// integration tests are run in test/integration/<test_name>/actual and the final test does
|
||||
// not clean up that directory so you can cd into it to see for yourself what
|
||||
// happened when a test failed.
|
||||
//
|
||||
// To run tests in parallel pass `PARALLEL=true` as an env var. Tests are run in parallel
|
||||
// on CI, and are run in a pty so you won't be able to see the stdout of the program
|
||||
// happened when a test fails.
|
||||
//
|
||||
// To override speed, pass e.g. `SPEED=1` as an env var. Otherwise we start each test
|
||||
// at a high speed and then drop down to lower speeds upon each failure until finally
|
||||
// trying at the original playback speed (speed 1). A speed of 2 represents twice the
|
||||
// original playback speed. Speed must be an integer.
|
||||
|
||||
type integrationTest struct {
|
||||
Name string `json:"name"`
|
||||
Speed int `json:"speed"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func loadTests(t *testing.T, testDir string) []*integrationTest {
|
||||
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := make([]*integrationTest, len(paths))
|
||||
|
||||
for i, path := range paths {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
test := &integrationTest{}
|
||||
|
||||
err = json.Unmarshal(data, test)
|
||||
assert.NoError(t, err)
|
||||
|
||||
test.Name = strings.TrimPrefix(filepath.Dir(path), testDir+"/")
|
||||
|
||||
tests[i] = test
|
||||
}
|
||||
|
||||
return tests
|
||||
}
|
||||
|
||||
func generateSnapshot(t *testing.T, dir string) string {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
_, err := os.Stat(filepath.Join(dir, ".git"))
|
||||
if err != nil {
|
||||
return "git directory not found"
|
||||
}
|
||||
|
||||
snapshot := ""
|
||||
|
||||
statusCmd := fmt.Sprintf(`git -C %s status`, dir)
|
||||
statusCmdOutput, err := osCommand.RunCommandWithOutput(statusCmd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
snapshot += statusCmdOutput + "\n"
|
||||
|
||||
logCmd := fmt.Sprintf(`git -C %s log --pretty=%%B -p -1`, dir)
|
||||
logCmdOutput, err := osCommand.RunCommandWithOutput(logCmd)
|
||||
assert.NoError(t, err)
|
||||
|
||||
snapshot += logCmdOutput + "\n"
|
||||
|
||||
err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
||||
assert.NoError(t, err)
|
||||
|
||||
if f.IsDir() {
|
||||
if f.Name() == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
snapshot += string(bytes) + "\n"
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func findOrCreateDir(path string) {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(path, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTestSpeeds(testStartSpeed int, updateSnapshots bool) []int {
|
||||
if updateSnapshots {
|
||||
// have to go at original speed if updating snapshots in case we go to fast and create a junk snapshot
|
||||
return []int{1}
|
||||
}
|
||||
|
||||
speedEnv := os.Getenv("SPEED")
|
||||
if speedEnv != "" {
|
||||
speed, err := strconv.Atoi(speedEnv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return []int{speed}
|
||||
}
|
||||
|
||||
// default is 10, 5, 1
|
||||
startSpeed := 10
|
||||
if testStartSpeed != 0 {
|
||||
startSpeed = testStartSpeed
|
||||
}
|
||||
speeds := []int{startSpeed}
|
||||
if startSpeed > 5 {
|
||||
speeds = append(speeds, 5)
|
||||
}
|
||||
speeds = append(speeds, 1)
|
||||
|
||||
return speeds
|
||||
}
|
||||
|
||||
func tempLazygitPath() string {
|
||||
return filepath.Join("/tmp", "lazygit", "test_lazygit")
|
||||
}
|
||||
// original playback speed. Speed may be a decimal.
|
||||
|
||||
func Test(t *testing.T) {
|
||||
rootDir := getRootDirectory()
|
||||
err := os.Chdir(rootDir)
|
||||
assert.NoError(t, err)
|
||||
record := false
|
||||
updateSnapshots := os.Getenv("UPDATE_SNAPSHOTS") != ""
|
||||
speedEnv := os.Getenv("SPEED")
|
||||
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
|
||||
|
||||
testDir := filepath.Join(rootDir, "test", "integration")
|
||||
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
err = osCommand.RunCommand("go build -o %s", tempLazygitPath())
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := loadTests(t, testDir)
|
||||
|
||||
record := os.Getenv("RECORD_EVENTS") != ""
|
||||
updateSnapshots := record || os.Getenv("UPDATE_SNAPSHOTS") != ""
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
if runInParallel() {
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
speeds := getTestSpeeds(test.Speed, updateSnapshots)
|
||||
|
||||
for i, speed := range speeds {
|
||||
t.Logf("%s: attempting test at speed %d\n", test.Name, speed)
|
||||
|
||||
testPath := filepath.Join(testDir, test.Name)
|
||||
actualDir := filepath.Join(testPath, "actual")
|
||||
expectedDir := filepath.Join(testPath, "expected")
|
||||
t.Logf("testPath: %s, actualDir: %s, expectedDir: %s", testPath, actualDir, expectedDir)
|
||||
findOrCreateDir(testPath)
|
||||
|
||||
prepareIntegrationTestDir(actualDir)
|
||||
|
||||
err := createFixture(testPath, actualDir)
|
||||
err := integration.RunTests(
|
||||
t.Logf,
|
||||
runCmdHeadless,
|
||||
func(test *integration.Test, f func(*testing.T) error) {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
err := f(t)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
},
|
||||
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))
|
||||
},
|
||||
includeSkipped,
|
||||
)
|
||||
|
||||
runLazygit(t, testPath, rootDir, record, speed)
|
||||
|
||||
if updateSnapshots {
|
||||
err = oscommands.CopyDir(actualDir, expectedDir)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
actual := generateSnapshot(t, actualDir)
|
||||
|
||||
expected := ""
|
||||
|
||||
func() {
|
||||
// git refuses to track .git folders in subdirectories so we need to rename it
|
||||
// to git_keep after running a test
|
||||
|
||||
defer func() {
|
||||
err = os.Rename(
|
||||
filepath.Join(expectedDir, ".git"),
|
||||
filepath.Join(expectedDir, ".git_keep"),
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}()
|
||||
|
||||
// ignoring this error because we might not have a .git_keep file here yet.
|
||||
_ = os.Rename(
|
||||
filepath.Join(expectedDir, ".git_keep"),
|
||||
filepath.Join(expectedDir, ".git"),
|
||||
)
|
||||
|
||||
expected = generateSnapshot(t, expectedDir)
|
||||
}()
|
||||
|
||||
if expected == actual {
|
||||
t.Logf("%s: success at speed %d\n", test.Name, speed)
|
||||
break
|
||||
}
|
||||
|
||||
// if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed
|
||||
if i == len(speeds)-1 {
|
||||
assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func createFixture(testPath, actualDir string) error {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
bashScriptPath := filepath.Join(testPath, "setup.sh")
|
||||
cmd := secureexec.Command("bash", bashScriptPath, actualDir)
|
||||
func runCmdHeadless(cmd *exec.Cmd) error {
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"HEADLESS=true",
|
||||
"TERM=xterm",
|
||||
)
|
||||
|
||||
if err := osCommand.RunExecutable(cmd); err != nil {
|
||||
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRootDirectory() string {
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
_, err := os.Stat(filepath.Join(path, ".git"))
|
||||
|
||||
if err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
path = filepath.Dir(path)
|
||||
|
||||
if path == "/" {
|
||||
panic("must run in lazygit folder or child folder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runLazygit(t *testing.T, testPath string, rootDir string, record bool, speed int) {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
replayPath := filepath.Join(testPath, "recording.json")
|
||||
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
|
||||
actualDir := filepath.Join(testPath, "actual")
|
||||
|
||||
exists, err := osCommand.FileExists(filepath.Join(testPath, "config"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
if exists {
|
||||
templateConfigDir = filepath.Join(testPath, "config")
|
||||
}
|
||||
|
||||
configDir := filepath.Join(testPath, "used_config")
|
||||
|
||||
err = os.RemoveAll(configDir)
|
||||
assert.NoError(t, err)
|
||||
err = oscommands.CopyDir(templateConfigDir, configDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmdStr := fmt.Sprintf("%s --use-config-dir=%s --path=%s", tempLazygitPath(), configDir, actualDir)
|
||||
|
||||
cmd := osCommand.ExecutableFromString(cmdStr)
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("REPLAY_SPEED=%d", speed))
|
||||
|
||||
if record {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath),
|
||||
)
|
||||
} else {
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath),
|
||||
)
|
||||
}
|
||||
|
||||
// if we're on CI we'll need to use a PTY. We can work that out by seeing if the 'TERM' env is defined.
|
||||
if runInPTY() {
|
||||
cmd.Env = append(cmd.Env, "TERM=xterm")
|
||||
|
||||
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, _ = io.Copy(ioutil.Discard, f)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
_ = f.Close()
|
||||
} else {
|
||||
err := cmd.Run()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func runInParallel() bool {
|
||||
return os.Getenv("PARALLEL") != ""
|
||||
}
|
||||
|
||||
func runInPTY() bool {
|
||||
return runInParallel() || os.Getenv("TERM") == ""
|
||||
}
|
||||
|
||||
func prepareIntegrationTestDir(actualDir string) {
|
||||
// remove contents of integration test directory
|
||||
dir, err := ioutil.ReadDir(actualDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(actualDir, 0777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, d := range dir {
|
||||
os.RemoveAll(filepath.Join(actualDir, d.Name()))
|
||||
}
|
||||
_, _ = io.Copy(ioutil.Discard, f)
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,32 +31,25 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
|
||||
minimumHeight := 9
|
||||
minimumWidth := 10
|
||||
if height < minimumHeight || width < minimumWidth {
|
||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.NotEnoughSpace
|
||||
v.Wrap = true
|
||||
_, _ = g.SetViewOnTop("limit")
|
||||
var err error
|
||||
gui.Views.Limit, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
|
||||
gui.Views.Limit.Wrap = true
|
||||
}
|
||||
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
|
||||
|
||||
informationStr := gui.informationStr()
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
|
||||
viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
|
||||
|
||||
_, _ = g.SetViewOnBottom("limit")
|
||||
_ = g.DeleteView("limit")
|
||||
|
||||
textColor := theme.GocuiDefaultTextColor
|
||||
|
||||
// reading more lines into main view buffers upon resize
|
||||
prevMainView, err := gui.g.View("main")
|
||||
if err == nil {
|
||||
prevMainView := gui.Views.Main
|
||||
if prevMainView != nil {
|
||||
_, prevMainHeight := prevMainView.Size()
|
||||
newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
|
||||
heightDiff := newMainHeight - prevMainHeight
|
||||
@@ -79,17 +72,17 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
// to render content as soon as it appears, because lazyloaded content (via a pty task)
|
||||
// cares about the size of the view.
|
||||
view, err := g.SetView(viewName, 0, 0, width, height, 0)
|
||||
if err != nil {
|
||||
return view, err
|
||||
if view != nil {
|
||||
view.Visible = false
|
||||
}
|
||||
return g.SetViewOnBottom(viewName)
|
||||
return view, err
|
||||
}
|
||||
|
||||
frameOffset := 1
|
||||
if frame {
|
||||
frameOffset = 0
|
||||
}
|
||||
return g.SetView(
|
||||
view, err := g.SetView(
|
||||
viewName,
|
||||
dimensionsObj.X0-frameOffset,
|
||||
dimensionsObj.Y0-frameOffset,
|
||||
@@ -97,231 +90,192 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
dimensionsObj.Y1+frameOffset,
|
||||
0,
|
||||
)
|
||||
|
||||
if view != nil {
|
||||
view.Visible = true
|
||||
}
|
||||
|
||||
return view, err
|
||||
}
|
||||
|
||||
v, err := setViewFromDimensions("main", "main", true)
|
||||
gui.Views.Main, err = setViewFromDimensions("main", "main", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.DiffTitle
|
||||
v.Wrap = true
|
||||
v.FgColor = textColor
|
||||
v.IgnoreCarriageReturns = true
|
||||
gui.Views.Main.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Main.Wrap = true
|
||||
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Main.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
secondaryView, err := setViewFromDimensions("secondary", "secondary", true)
|
||||
gui.Views.Secondary, err = setViewFromDimensions("secondary", "secondary", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
secondaryView.Title = gui.Tr.DiffTitle
|
||||
secondaryView.Wrap = true
|
||||
secondaryView.FgColor = textColor
|
||||
secondaryView.IgnoreCarriageReturns = true
|
||||
gui.Views.Secondary.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Secondary.Wrap = true
|
||||
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Secondary.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
hiddenViewOffset := 9999
|
||||
|
||||
if v, err := setViewFromDimensions("status", "status", true); err != nil {
|
||||
if gui.Views.Status, err = setViewFromDimensions("status", "status", true); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.StatusTitle
|
||||
v.FgColor = textColor
|
||||
gui.Views.Status.Title = gui.Tr.StatusTitle
|
||||
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
|
||||
}
|
||||
|
||||
filesView, err := setViewFromDimensions("files", "files", true)
|
||||
gui.Views.Files, err = setViewFromDimensions("files", "files", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
filesView.Highlight = true
|
||||
filesView.Title = gui.Tr.FilesTitle
|
||||
filesView.FgColor = textColor
|
||||
filesView.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
|
||||
}
|
||||
|
||||
branchesView, err := setViewFromDimensions("branches", "branches", true)
|
||||
gui.Views.Branches, err = setViewFromDimensions("branches", "branches", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
branchesView.Title = gui.Tr.BranchesTitle
|
||||
branchesView.FgColor = textColor
|
||||
branchesView.ContainsList = true
|
||||
gui.Views.Branches.Title = gui.Tr.BranchesTitle
|
||||
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Branches.ContainsList = true
|
||||
}
|
||||
|
||||
commitFilesView, err := setViewFromDimensions("commitFiles", gui.Contexts.CommitFiles.Context.GetWindowName(), true)
|
||||
gui.Views.CommitFiles, err = setViewFromDimensions("commitFiles", gui.State.Contexts.CommitFiles.GetWindowName(), true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
commitFilesView.Title = gui.Tr.CommitFiles
|
||||
commitFilesView.FgColor = textColor
|
||||
commitFilesView.ContainsList = true
|
||||
_, _ = gui.g.SetViewOnBottom("commitFiles")
|
||||
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
|
||||
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitFiles.ContainsList = true
|
||||
}
|
||||
// if the commit files view is the view to be displayed for its window, we'll display it
|
||||
gui.Views.CommitFiles.Visible = gui.getViewNameForWindow(gui.State.Contexts.CommitFiles.GetWindowName()) == "commitFiles"
|
||||
|
||||
commitsView, err := setViewFromDimensions("commits", "commits", true)
|
||||
gui.Views.Commits, err = setViewFromDimensions("commits", "commits", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
commitsView.Title = gui.Tr.CommitsTitle
|
||||
commitsView.FgColor = textColor
|
||||
commitsView.ContainsList = true
|
||||
gui.Views.Commits.Title = gui.Tr.CommitsTitle
|
||||
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Commits.ContainsList = true
|
||||
}
|
||||
|
||||
stashView, err := setViewFromDimensions("stash", "stash", true)
|
||||
gui.Views.Stash, err = setViewFromDimensions("stash", "stash", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
stashView.Title = gui.Tr.StashTitle
|
||||
stashView.FgColor = textColor
|
||||
stashView.ContainsList = true
|
||||
gui.Views.Stash.Title = gui.Tr.StashTitle
|
||||
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Stash.ContainsList = true
|
||||
}
|
||||
|
||||
if gui.getCommitMessageView() == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = gui.Tr.CommitMessage
|
||||
commitMessageView.FgColor = textColor
|
||||
commitMessageView.Editable = true
|
||||
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
}
|
||||
}
|
||||
|
||||
if check, _ := g.View("credentials"); check == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("credentials")
|
||||
credentialsView.Title = gui.Tr.CredentialsUsername
|
||||
credentialsView.FgColor = textColor
|
||||
credentialsView.Editable = true
|
||||
}
|
||||
}
|
||||
|
||||
if v, err := setViewFromDimensions("options", "options", false); err != nil {
|
||||
if gui.Views.Options, err = setViewFromDimensions("options", "options", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
v.Frame = false
|
||||
v.FgColor = theme.OptionsColor
|
||||
|
||||
// doing this here because it'll only happen once
|
||||
if err := gui.onInitialViewsCreation(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.Options.Frame = false
|
||||
gui.Views.Options.FgColor = theme.OptionsColor
|
||||
}
|
||||
|
||||
// this view takes up one character. Its only purpose is to show the slash when searching
|
||||
if searchPrefixView, err := setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
|
||||
if gui.Views.SearchPrefix, err = setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
|
||||
searchPrefixView.BgColor = gocui.ColorDefault
|
||||
searchPrefixView.FgColor = gocui.ColorGreen
|
||||
searchPrefixView.Frame = false
|
||||
gui.setViewContent(searchPrefixView, SEARCH_PREFIX)
|
||||
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
|
||||
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
|
||||
gui.Views.SearchPrefix.Frame = false
|
||||
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
|
||||
}
|
||||
|
||||
if searchView, err := setViewFromDimensions("search", "search", false); err != nil {
|
||||
if gui.Views.Search, err = setViewFromDimensions("search", "search", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
|
||||
searchView.BgColor = gocui.ColorDefault
|
||||
searchView.FgColor = gocui.ColorGreen
|
||||
searchView.Frame = false
|
||||
searchView.Editable = true
|
||||
gui.Views.Search.BgColor = gocui.ColorDefault
|
||||
gui.Views.Search.FgColor = gocui.ColorGreen
|
||||
gui.Views.Search.Frame = false
|
||||
gui.Views.Search.Editable = true
|
||||
}
|
||||
|
||||
if appStatusView, err := setViewFromDimensions("appStatus", "appStatus", false); err != nil {
|
||||
if gui.Views.AppStatus, err = setViewFromDimensions("appStatus", "appStatus", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
appStatusView.BgColor = gocui.ColorDefault
|
||||
appStatusView.FgColor = gocui.ColorCyan
|
||||
appStatusView.Frame = false
|
||||
_, _ = g.SetViewOnBottom("appStatus")
|
||||
gui.Views.AppStatus.BgColor = gocui.ColorDefault
|
||||
gui.Views.AppStatus.FgColor = gocui.ColorCyan
|
||||
gui.Views.AppStatus.Frame = false
|
||||
gui.Views.AppStatus.Visible = false
|
||||
}
|
||||
|
||||
informationView, err := setViewFromDimensions("information", "information", false)
|
||||
gui.Views.Information, err = setViewFromDimensions("information", "information", false)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
informationView.BgColor = gocui.ColorDefault
|
||||
informationView.FgColor = gocui.ColorGreen
|
||||
informationView.Frame = false
|
||||
gui.renderString("information", INFO_SECTION_PADDING+informationStr)
|
||||
gui.Views.Information.BgColor = gocui.ColorDefault
|
||||
gui.Views.Information.FgColor = gocui.ColorGreen
|
||||
gui.Views.Information.Frame = false
|
||||
gui.renderString(gui.Views.Information, INFO_SECTION_PADDING+informationStr)
|
||||
}
|
||||
if gui.State.OldInformation != informationStr {
|
||||
gui.setViewContent(informationView, informationStr)
|
||||
gui.setViewContent(gui.Views.Information, informationStr)
|
||||
gui.State.OldInformation = informationStr
|
||||
}
|
||||
|
||||
if gui.g.CurrentView() == nil {
|
||||
initialContext := gui.Contexts.Files.Context
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
initialContext = gui.Contexts.BranchCommits.Context
|
||||
}
|
||||
|
||||
if err := gui.pushContext(initialContext); err != nil {
|
||||
if !gui.ViewsSetup {
|
||||
if err := gui.onInitialViewsCreation(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.ViewsSetup = true
|
||||
}
|
||||
|
||||
type listContextState struct {
|
||||
view *gocui.View
|
||||
listContext *ListContext
|
||||
if !gui.State.ViewsSetup {
|
||||
if err := gui.onInitialViewsCreationForRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.ViewsSetup = true
|
||||
}
|
||||
|
||||
// TODO: don't we already have the view included in the context object itself? Or might that change in a way we don't want reflected here?
|
||||
listContextStates := []listContextState{
|
||||
{view: filesView, listContext: gui.filesListContext()},
|
||||
{view: filesView, listContext: gui.submodulesListContext()},
|
||||
{view: branchesView, listContext: gui.branchesListContext()},
|
||||
{view: branchesView, listContext: gui.remotesListContext()},
|
||||
{view: branchesView, listContext: gui.remoteBranchesListContext()},
|
||||
{view: branchesView, listContext: gui.tagsListContext()},
|
||||
{view: commitsView, listContext: gui.branchCommitsListContext()},
|
||||
{view: commitsView, listContext: gui.reflogCommitsListContext()},
|
||||
{view: stashView, listContext: gui.stashListContext()},
|
||||
{view: commitFilesView, listContext: gui.commitFilesListContext()},
|
||||
}
|
||||
|
||||
// menu view might not exist so we check to be safe
|
||||
if menuView, err := gui.g.View("menu"); err == nil {
|
||||
listContextStates = append(listContextStates, listContextState{view: menuView, listContext: gui.menuListContext()})
|
||||
}
|
||||
for _, listContextState := range listContextStates {
|
||||
// ignore contexts whose view is owned by another context right now
|
||||
if listContextState.view.Context != listContextState.listContext.GetKey() {
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
view, err := gui.g.View(listContext.ViewName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
listContextState.view.FocusPoint(0, listContextState.listContext.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
listContextState.view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
// ignore contexts whose view is owned by another context right now
|
||||
if ContextKey(view.Context) != listContext.GetKey() {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
view.FocusPoint(0, listContext.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
|
||||
// I doubt this is expensive though it's admittedly redundant after the first render
|
||||
listContextState.view.SetOnSelectItem(gui.onSelectItemWrapper(listContextState.listContext.onSearchSelect))
|
||||
view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.onSearchSelect))
|
||||
}
|
||||
|
||||
gui.getMainView().SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
|
||||
gui.Views.Main.SetOnSelectItem(gui.onSelectItemWrapper(gui.handlelineByLineNavigateTo))
|
||||
|
||||
mainViewWidth, mainViewHeight := gui.getMainView().Size()
|
||||
mainViewWidth, mainViewHeight := gui.Views.Main.Size()
|
||||
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
|
||||
gui.State.PrevMainWidth = mainViewWidth
|
||||
gui.State.PrevMainHeight = mainViewHeight
|
||||
@@ -337,11 +291,77 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return gui.resizeCurrentPopupPanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreation() error {
|
||||
func (gui *Gui) setHiddenView(viewName string) (*gocui.View, error) {
|
||||
// arbitrarily giving the view enough size so that we don't get an error, but
|
||||
// it's expected that the view will be given the correct size before being shown
|
||||
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreationForRepo() error {
|
||||
gui.setInitialViewContexts()
|
||||
|
||||
// add tabs to views
|
||||
// hide any popup views. This only applies when we've just switched repos
|
||||
for _, viewName := range gui.popupViewNames() {
|
||||
view, err := gui.g.View(viewName)
|
||||
if err == nil {
|
||||
view.Visible = false
|
||||
}
|
||||
}
|
||||
|
||||
initialContext := gui.currentSideContext()
|
||||
if err := gui.pushContext(initialContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.loadNewRepo()
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreation() error {
|
||||
// creating some views which are hidden at the start but we need to exist so that we can set an initial ordering
|
||||
if err := gui.createHiddenViews(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now we order the views (in order of bottom first)
|
||||
layerOneViews := []*gocui.View{
|
||||
// first layer. Ordering within this layer does not matter because there are
|
||||
// no overlapping views
|
||||
gui.Views.Status,
|
||||
gui.Views.Files,
|
||||
gui.Views.Branches,
|
||||
gui.Views.Commits,
|
||||
gui.Views.Stash,
|
||||
gui.Views.CommitFiles,
|
||||
gui.Views.Main,
|
||||
gui.Views.Secondary,
|
||||
|
||||
// bottom line
|
||||
gui.Views.Options,
|
||||
gui.Views.AppStatus,
|
||||
gui.Views.Information,
|
||||
gui.Views.Search,
|
||||
gui.Views.SearchPrefix,
|
||||
|
||||
// popups. Ordering within this layer does not matter because there should
|
||||
// only be one popup shown at a time
|
||||
gui.Views.CommitMessage,
|
||||
gui.Views.Credentials,
|
||||
gui.Views.Menu,
|
||||
gui.Views.Suggestions,
|
||||
gui.Views.Confirmation,
|
||||
|
||||
// this guy will cover everything else when it appears
|
||||
gui.Views.Limit,
|
||||
}
|
||||
|
||||
for _, view := range layerOneViews {
|
||||
if _, err := gui.g.SetViewOnTop(view.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.g.Mutexes.ViewsMutex.Lock()
|
||||
// add tabs to views
|
||||
for _, view := range gui.g.Views() {
|
||||
tabs := gui.viewTabNames(view.Name())
|
||||
if len(tabs) == 0 {
|
||||
@@ -351,10 +371,6 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
}
|
||||
gui.g.Mutexes.ViewsMutex.Unlock()
|
||||
|
||||
if err := gui.pushContext(gui.defaultSideContext()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.keybindings(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -366,5 +382,64 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
gui.showRecentRepos = false
|
||||
}
|
||||
|
||||
return gui.loadNewRepo()
|
||||
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
|
||||
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createHiddenViews() error {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
var err error
|
||||
if gui.Views.CommitMessage, err = gui.setHiddenView("commitMessage"); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitMessage.Visible = false
|
||||
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
|
||||
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitMessage.Editable = true
|
||||
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
}
|
||||
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if gui.Views.Credentials, err = gui.setHiddenView("credentials"); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Credentials.Visible = false
|
||||
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
|
||||
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Credentials.Editable = true
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Confirmation, err = gui.setHiddenView("confirmation")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.Visible = false
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Suggestions, err = gui.setHiddenView("suggestions")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Suggestions.Visible = false
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Menu, err = gui.setHiddenView("menu")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Menu.Visible = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
// use cases
|
||||
|
||||
// these represent what select mode we're in
|
||||
type SelectMode int
|
||||
|
||||
const (
|
||||
LINE = iota
|
||||
LINE SelectMode = iota
|
||||
RANGE
|
||||
HUNK
|
||||
)
|
||||
@@ -25,6 +27,8 @@ const (
|
||||
// returns whether the patch is empty so caller can escape if necessary
|
||||
// both diffs should be non-coloured because we'll parse them and colour them here
|
||||
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int, state *lBlPanelState) (bool, error) {
|
||||
gui.splitMainPanel(true)
|
||||
|
||||
patchParser, err := patch.NewPatchParser(gui.Log, diff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
@@ -80,9 +84,8 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
return false, err
|
||||
}
|
||||
|
||||
secondaryView := gui.getSecondaryView()
|
||||
secondaryView.Highlight = true
|
||||
secondaryView.Wrap = false
|
||||
gui.Views.Secondary.Highlight = true
|
||||
gui.Views.Secondary.Wrap = false
|
||||
|
||||
secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff)
|
||||
if err != nil {
|
||||
@@ -90,7 +93,7 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
gui.setViewContent(gui.Views.Secondary, secondaryPatchParser.Render(-1, -1, nil))
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -176,13 +179,13 @@ func (gui *Gui) LBLSelectLine(newSelectedLineIdx int, state *lBlPanelState) erro
|
||||
return gui.focusSelection(false, state)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLBLMouseDown(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleLBLMouseDown() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
newSelectedLineIdx := gui.Views.Main.SelectedLineIdx()
|
||||
state.FirstLineIdx = newSelectedLineIdx
|
||||
state.LastLineIdx = newSelectedLineIdx
|
||||
|
||||
@@ -192,49 +195,27 @@ func (gui *Gui) handleLBLMouseDown(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMouseDrag() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.LBLSelectLine(v.SelectedLineIdx(), state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.SelectMode = LINE
|
||||
|
||||
return gui.LBLCycleLine(-1, state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.SelectMode = LINE
|
||||
|
||||
return gui.LBLCycleLine(1, state)
|
||||
return gui.LBLSelectLine(gui.Views.Main.SelectedLineIdx(), state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFileName() string {
|
||||
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
|
||||
idx := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(idx).GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
var includedLineIndices []int
|
||||
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
|
||||
// how to get around this
|
||||
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
|
||||
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
var err error
|
||||
includedLineIndices, err = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
@@ -244,12 +225,11 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
}
|
||||
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
|
||||
|
||||
mainView := gui.getMainView()
|
||||
mainView.Highlight = true
|
||||
mainView.Wrap = false
|
||||
gui.Views.Main.Highlight = true
|
||||
gui.Views.Main.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.getMainView(), colorDiff)
|
||||
gui.setViewContent(gui.Views.Main, colorDiff)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -259,7 +239,7 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
// focusSelection works out the best focus for the staging panel given the
|
||||
// selected line and size of the hunk
|
||||
func (gui *Gui) focusSelection(includeCurrentHunk bool, state *lBlPanelState) error {
|
||||
stagingView := gui.getMainView()
|
||||
stagingView := gui.Views.Main
|
||||
|
||||
_, viewHeight := stagingView.Size()
|
||||
bufferHeight := viewHeight - 1
|
||||
@@ -337,9 +317,9 @@ func (gui *Gui) handleOpenFileAtLine() error {
|
||||
// again, would be good to use inheritance here (or maybe even composition)
|
||||
var filename string
|
||||
switch gui.State.MainContext {
|
||||
case gui.Contexts.PatchBuilding.Context.GetKey():
|
||||
case gui.State.Contexts.PatchBuilding.GetKey():
|
||||
filename = gui.getSelectedCommitFileName()
|
||||
case gui.Contexts.Staging.Context.GetKey():
|
||||
case gui.State.Contexts.Staging.GetKey():
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
@@ -363,7 +343,7 @@ func (gui *Gui) handleOpenFileAtLine() error {
|
||||
|
||||
func (gui *Gui) handleLineByLineNextPage() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.getMainView())
|
||||
newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.Views.Main)
|
||||
|
||||
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
|
||||
})
|
||||
@@ -371,7 +351,7 @@ func (gui *Gui) handleLineByLineNextPage() error {
|
||||
|
||||
func (gui *Gui) handleLineByLinePrevPage() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.getMainView())
|
||||
newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.Views.Main)
|
||||
|
||||
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
|
||||
})
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type ListContext struct {
|
||||
ViewName string
|
||||
ContextKey string
|
||||
ContextKey ContextKey
|
||||
GetItemsLength func() int
|
||||
GetDisplayStrings func() [][]string
|
||||
OnFocus func() error
|
||||
@@ -21,13 +23,18 @@ type ListContext struct {
|
||||
|
||||
Gui *Gui
|
||||
ResetMainViewOriginOnFocus bool
|
||||
Kind int
|
||||
Kind ContextKind
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this.
|
||||
hasParent bool
|
||||
WindowName string
|
||||
}
|
||||
|
||||
type IListPanelState interface {
|
||||
SetSelectedLineIdx(int)
|
||||
GetSelectedLineIdx() int
|
||||
}
|
||||
|
||||
type ListItem interface {
|
||||
// ID is a SHA when the item is a commit, a filename when the item is a file, 'stash@{4}' when it's a stash entry, 'my_branch' when it's a branch
|
||||
ID() string
|
||||
@@ -96,11 +103,11 @@ func (lc *ListContext) OnRender() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKey() string {
|
||||
func (lc *ListContext) GetKey() ContextKey {
|
||||
return lc.ContextKey
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKind() int {
|
||||
func (lc *ListContext) GetKind() ContextKind {
|
||||
return lc.Kind
|
||||
}
|
||||
|
||||
@@ -128,6 +135,15 @@ func (lc *ListContext) HandleFocus() error {
|
||||
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -143,11 +159,11 @@ func (lc *ListContext) HandleRender() error {
|
||||
return lc.OnRender()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handlePrevLine() error {
|
||||
return lc.handleLineChange(-1)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handleNextLine() error {
|
||||
return lc.handleLineChange(1)
|
||||
}
|
||||
|
||||
@@ -169,19 +185,10 @@ func (lc *ListContext) handleLineChange(change int) error {
|
||||
lc.Gui.changeSelectedLine(lc.GetPanelState(), lc.GetItemsLength(), change)
|
||||
view.FocusPoint(0, lc.GetPanelState().GetSelectedLineIdx())
|
||||
|
||||
if lc.ResetMainViewOriginOnFocus {
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := lc.Gui.resetOrigin(lc.Gui.getSecondaryView()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return lc.HandleFocus()
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleNextPage(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handleNextPage() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -191,15 +198,15 @@ func (lc *ListContext) handleNextPage(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handleGotoTop() error {
|
||||
return lc.handleLineChange(-lc.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handleGotoBottom() error {
|
||||
return lc.handleLineChange(lc.GetItemsLength())
|
||||
}
|
||||
|
||||
func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handlePrevPage() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -210,13 +217,18 @@ func (lc *ListContext) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
|
||||
return lc.handleLineChange(-delta)
|
||||
}
|
||||
|
||||
func (lc *ListContext) handleClick(g *gocui.Gui, v *gocui.View) error {
|
||||
func (lc *ListContext) handleClick() error {
|
||||
if !lc.Gui.isPopupPanel(lc.ViewName) && lc.Gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSelectedLineIdx := lc.GetPanelState().GetSelectedLineIdx()
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
newSelectedLineIdx := view.SelectedLineIdx()
|
||||
|
||||
// we need to focus the view
|
||||
if err := lc.Gui.pushContext(lc); err != nil {
|
||||
@@ -245,7 +257,7 @@ func (gui *Gui) menuListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "menu",
|
||||
ContextKey: "menu",
|
||||
GetItemsLength: func() int { return gui.getMenuView().LinesHeight() },
|
||||
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: gui.onMenuPress,
|
||||
@@ -262,7 +274,7 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "files",
|
||||
ContextKey: FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Files) },
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
@@ -270,10 +282,16 @@ func (gui *Gui) filesListContext() *ListContext {
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedFile()
|
||||
item := gui.getSelectedFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
@@ -446,17 +464,27 @@ func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
ViewName: "commitFiles",
|
||||
WindowName: "commits",
|
||||
ContextKey: COMMIT_FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.CommitFiles) },
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Modes.Diffing.Ref)
|
||||
if gui.State.CommitFileManager.GetItemsLength() == 0 {
|
||||
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
|
||||
}
|
||||
|
||||
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedCommitFile()
|
||||
item := gui.getSelectedCommitFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
@@ -502,19 +530,20 @@ func (gui *Gui) suggestionsListContext() *ListContext {
|
||||
|
||||
func (gui *Gui) getListContexts() []*ListContext {
|
||||
return []*ListContext{
|
||||
gui.menuListContext(),
|
||||
gui.filesListContext(),
|
||||
gui.branchesListContext(),
|
||||
gui.remotesListContext(),
|
||||
gui.remoteBranchesListContext(),
|
||||
gui.tagsListContext(),
|
||||
gui.branchCommitsListContext(),
|
||||
gui.reflogCommitsListContext(),
|
||||
gui.subCommitsListContext(),
|
||||
gui.stashListContext(),
|
||||
gui.commitFilesListContext(),
|
||||
gui.submodulesListContext(),
|
||||
gui.suggestionsListContext(),
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
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,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,17 +553,19 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
listContext := listContext
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{listContext.ContextKey}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{listContext.ContextKey}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, 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.ContextKey)}, 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.ContextKey)}, 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.ContextKey)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
}...)
|
||||
|
||||
// the commits panel needs to lazyload things so it has a couple of its own handlers
|
||||
@@ -548,15 +579,15 @@ func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
bindings = append(bindings, []*Binding{
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{listContext.ContextKey},
|
||||
Contexts: []string{string(listContext.ContextKey)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
|
||||
Handler: openSearchHandler,
|
||||
Handler: func() error { return openSearchHandler(listContext.ViewName) },
|
||||
Description: gui.Tr.LcStartSearch,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{listContext.ContextKey},
|
||||
Contexts: []string{string(listContext.ContextKey)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
|
||||
Handler: gotoBottomHandler,
|
||||
Description: gui.Tr.LcGotoBottom,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package gui
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type viewUpdateOpts struct {
|
||||
title string
|
||||
@@ -20,8 +24,10 @@ type refreshMainOpts struct {
|
||||
}
|
||||
|
||||
// constants for updateTask's kind field
|
||||
type TaskKind int
|
||||
|
||||
const (
|
||||
RENDER_STRING = iota
|
||||
RENDER_STRING TaskKind = iota
|
||||
RENDER_STRING_WITHOUT_SCROLL
|
||||
RUN_FUNCTION
|
||||
RUN_COMMAND
|
||||
@@ -29,18 +35,18 @@ const (
|
||||
)
|
||||
|
||||
type updateTask interface {
|
||||
GetKind() int
|
||||
GetKind() TaskKind
|
||||
}
|
||||
|
||||
type renderStringTask struct {
|
||||
str string
|
||||
}
|
||||
|
||||
func (t *renderStringTask) GetKind() int {
|
||||
func (t *renderStringTask) GetKind() TaskKind {
|
||||
return RENDER_STRING
|
||||
}
|
||||
|
||||
func (gui *Gui) createRenderStringTask(str string) *renderStringTask {
|
||||
func NewRenderStringTask(str string) *renderStringTask {
|
||||
return &renderStringTask{str: str}
|
||||
}
|
||||
|
||||
@@ -48,11 +54,11 @@ type renderStringWithoutScrollTask struct {
|
||||
str string
|
||||
}
|
||||
|
||||
func (t *renderStringWithoutScrollTask) GetKind() int {
|
||||
func (t *renderStringWithoutScrollTask) GetKind() TaskKind {
|
||||
return RENDER_STRING_WITHOUT_SCROLL
|
||||
}
|
||||
|
||||
func (gui *Gui) createRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
|
||||
func NewRenderStringWithoutScrollTask(str string) *renderStringWithoutScrollTask {
|
||||
return &renderStringWithoutScrollTask{str: str}
|
||||
}
|
||||
|
||||
@@ -61,15 +67,15 @@ type runCommandTask struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (t *runCommandTask) GetKind() int {
|
||||
func (t *runCommandTask) GetKind() TaskKind {
|
||||
return RUN_COMMAND
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunCommandTask(cmd *exec.Cmd) *runCommandTask {
|
||||
func NewRunCommandTask(cmd *exec.Cmd) *runCommandTask {
|
||||
return &runCommandTask{cmd: cmd}
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunCommandTaskWithPrefix(cmd *exec.Cmd, prefix string) *runCommandTask {
|
||||
func NewRunCommandTaskWithPrefix(cmd *exec.Cmd, prefix string) *runCommandTask {
|
||||
return &runCommandTask{cmd: cmd, prefix: prefix}
|
||||
}
|
||||
|
||||
@@ -78,11 +84,11 @@ type runPtyTask struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (t *runPtyTask) GetKind() int {
|
||||
func (t *runPtyTask) GetKind() TaskKind {
|
||||
return RUN_PTY
|
||||
}
|
||||
|
||||
func (gui *Gui) createRunPtyTask(cmd *exec.Cmd) *runPtyTask {
|
||||
func NewRunPtyTask(cmd *exec.Cmd) *runPtyTask {
|
||||
return &runPtyTask{cmd: cmd}
|
||||
}
|
||||
|
||||
@@ -95,7 +101,7 @@ type runFunctionTask struct {
|
||||
f func(chan struct{}) error
|
||||
}
|
||||
|
||||
func (t *runFunctionTask) GetKind() int {
|
||||
func (t *runFunctionTask) GetKind() TaskKind {
|
||||
return RUN_FUNCTION
|
||||
}
|
||||
|
||||
@@ -104,44 +110,38 @@ func (t *runFunctionTask) GetKind() int {
|
||||
// return &runFunctionTask{f: f}
|
||||
// }
|
||||
|
||||
func (gui *Gui) runTaskForView(viewName string, task updateTask) error {
|
||||
func (gui *Gui) runTaskForView(view *gocui.View, task updateTask) error {
|
||||
switch task.GetKind() {
|
||||
case RENDER_STRING:
|
||||
specificTask := task.(*renderStringTask)
|
||||
return gui.newStringTask(viewName, specificTask.str)
|
||||
return gui.newStringTask(view, specificTask.str)
|
||||
|
||||
case RENDER_STRING_WITHOUT_SCROLL:
|
||||
specificTask := task.(*renderStringWithoutScrollTask)
|
||||
return gui.newStringTaskWithoutScroll(viewName, specificTask.str)
|
||||
return gui.newStringTaskWithoutScroll(view, specificTask.str)
|
||||
|
||||
case RUN_FUNCTION:
|
||||
specificTask := task.(*runFunctionTask)
|
||||
return gui.newTask(viewName, specificTask.f)
|
||||
return gui.newTask(view, specificTask.f)
|
||||
|
||||
case RUN_COMMAND:
|
||||
specificTask := task.(*runCommandTask)
|
||||
return gui.newCmdTask(viewName, specificTask.cmd, specificTask.prefix)
|
||||
return gui.newCmdTask(view, specificTask.cmd, specificTask.prefix)
|
||||
|
||||
case RUN_PTY:
|
||||
specificTask := task.(*runPtyTask)
|
||||
return gui.newPtyTask(viewName, specificTask.cmd, specificTask.prefix)
|
||||
return gui.newPtyTask(view, specificTask.cmd, specificTask.prefix)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView(opts *viewUpdateOpts, viewName string) error {
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView(opts *viewUpdateOpts, view *gocui.View) error {
|
||||
view.Title = opts.title
|
||||
view.Wrap = !opts.noWrap
|
||||
view.Highlight = opts.highlight
|
||||
|
||||
if err := gui.runTaskForView(viewName, opts.task); err != nil {
|
||||
if err := gui.runTaskForView(view, opts.task); err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
@@ -151,29 +151,24 @@ func (gui *Gui) refreshMainView(opts *viewUpdateOpts, viewName string) error {
|
||||
|
||||
func (gui *Gui) refreshMainViews(opts refreshMainOpts) error {
|
||||
if opts.main != nil {
|
||||
if err := gui.refreshMainView(opts.main, "main"); err != nil {
|
||||
if err := gui.refreshMainView(opts.main, gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.secondary != nil {
|
||||
if err := gui.refreshMainView(opts.secondary, gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.splitMainPanel(opts.secondary != nil)
|
||||
|
||||
if opts.secondary != nil {
|
||||
if err := gui.refreshMainView(opts.secondary, "secondary"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) splitMainPanel(splitMainPanel bool) {
|
||||
gui.State.SplitMainPanel = splitMainPanel
|
||||
|
||||
// no need to set view on bottom when splitMainPanel is false: it will have zero size anyway thanks to our view arrangement code.
|
||||
if splitMainPanel {
|
||||
_, _ = gui.g.SetViewOnTop("secondary")
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) isMainPanelSplit() bool {
|
||||
|
||||
@@ -42,8 +42,7 @@ func (gui *Gui) getMenuOptions() map[string]string {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
_ = g.DeleteView("menu")
|
||||
func (gui *Gui) handleMenuClose() error {
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
@@ -88,7 +87,7 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
gui.State.Panels.Menu.SelectedLineIdx = 0
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.pushContext(gui.Contexts.Menu.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Menu)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,120 +4,146 @@ package gui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
)
|
||||
|
||||
func (gui *Gui) findConflicts(content string) []commands.Conflict {
|
||||
conflicts := make([]commands.Conflict, 0)
|
||||
func (gui *Gui) handleSelectTop() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = true
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
func (gui *Gui) handleSelectBottom() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = false
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
var newConflict commands.Conflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
|
||||
newConflict = commands.Conflict{Start: i}
|
||||
} else if trimmedLine == "=======" {
|
||||
newConflict.Middle = i
|
||||
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
||||
newConflict.End = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
func (gui *Gui) handleSelectNextConflict() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return conflicts
|
||||
gui.State.Panels.Merging.ConflictIndex++
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
||||
}
|
||||
|
||||
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
|
||||
if len(conflicts) == 0 {
|
||||
return content
|
||||
}
|
||||
conflict, remainingConflicts := gui.shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := theme.DefaultTextColor
|
||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||
colourAttr = color.FgRed
|
||||
func (gui *Gui) handleSelectPrevConflict() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String()
|
||||
gui.State.Panels.Merging.ConflictIndex--
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) takeOverScrolling() {
|
||||
gui.State.Panels.Merging.UserScrolling = false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = true
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = false
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
||||
func (gui *Gui) pushFileSnapshot() error {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
gui.State.Panels.Merging.ConflictIndex++
|
||||
return gui.refreshMergePanel()
|
||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Panels.Merging.EditHistory.Push(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
||||
func (gui *Gui) handlePopFileSnapshot() error {
|
||||
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
gui.State.Panels.Merging.ConflictIndex--
|
||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
|
||||
return i == conflict.Middle ||
|
||||
i == conflict.Start ||
|
||||
i == conflict.End ||
|
||||
pick != "both" &&
|
||||
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
|
||||
(pick == "top" && i > conflict.Middle && i < conflict.End)
|
||||
func (gui *Gui) handlePickHunk() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
conflict := gui.getCurrentConflict()
|
||||
if conflict == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selection := mergeconflicts.BOTTOM
|
||||
if gui.State.Panels.Merging.ConflictTop {
|
||||
selection = mergeconflicts.TOP
|
||||
}
|
||||
err := gui.resolveConflict(*conflict, selection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// if that was the last conflict, finish the merge for this file
|
||||
if len(gui.State.Panels.Merging.Conflicts) == 1 {
|
||||
if err := gui.handleCompleteMerge(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
|
||||
func (gui *Gui) handlePickBothHunks() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
conflict := gui.getCurrentConflict()
|
||||
if conflict == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.resolveConflict(*conflict, mergeconflicts.BOTH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getCurrentConflict() *commands.Conflict {
|
||||
if len(gui.State.Panels.Merging.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconflicts.Selection) error {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
@@ -135,80 +161,15 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !gui.isIndexToDelete(i, conflict, pick) {
|
||||
if !mergeconflicts.IsIndexToDelete(i, conflict, selection) {
|
||||
output += line
|
||||
}
|
||||
}
|
||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFileSnapshot() error {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Panels.Merging.EditHistory.Push(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pick := "bottom"
|
||||
if gui.State.Panels.Merging.ConflictTop {
|
||||
pick = "top"
|
||||
}
|
||||
err := gui.resolveConflict(conflict, pick)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// if that was the last conflict, finish the merge for this file
|
||||
if len(gui.State.Panels.Merging.Conflicts) == 1 {
|
||||
if err := gui.handleCompleteMerge(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.resolveConflict(conflict, "both")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
func (gui *Gui) refreshMergePanelWithLock() error {
|
||||
return gui.withMergeConflictLock(gui.refreshMergePanel)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMergePanel() error {
|
||||
@@ -218,12 +179,12 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(err.Error()),
|
||||
task: NewRenderStringTask(err.Error()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
panelState.Conflicts = gui.findConflicts(cat)
|
||||
panelState.Conflicts = mergeconflicts.FindConflicts(cat)
|
||||
|
||||
// handle potential fixes that the user made in their editor since we last refreshed
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
@@ -233,7 +194,7 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
}
|
||||
|
||||
hasFocus := gui.currentViewName() == "main"
|
||||
content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
|
||||
content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
|
||||
|
||||
if err := gui.scrollToConflict(); err != nil {
|
||||
return err
|
||||
@@ -242,7 +203,7 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: gui.Tr.MergeConflictsTitle,
|
||||
task: gui.createRenderStringWithoutScrollTask(content),
|
||||
task: NewRenderStringWithoutScrollTask(content),
|
||||
noWrap: true,
|
||||
},
|
||||
})
|
||||
@@ -275,7 +236,8 @@ func (gui *Gui) scrollToConflict() error {
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
mergingView := gui.getMainView()
|
||||
|
||||
mergingView := gui.Views.Main
|
||||
conflict := panelState.Conflicts[panelState.ConflictIndex]
|
||||
ox, _ := mergingView.Origin()
|
||||
_, height := mergingView.Size()
|
||||
@@ -300,16 +262,16 @@ func (gui *Gui) getMergingOptions() map[string]string {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeMerge() error {
|
||||
gui.takeOverScrolling()
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
gui.State.Panels.Merging.EditHistory = stack.New()
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
// it's possible this method won't be called from the merging view so we need to
|
||||
// ensure we only 'return' focus if we already have it
|
||||
if gui.g.CurrentView() == gui.getMainView() {
|
||||
return gui.pushContext(gui.Contexts.Files.Context)
|
||||
if gui.g.CurrentView() == gui.Views.Main {
|
||||
return gui.pushContext(gui.State.Contexts.Files)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -318,7 +280,7 @@ func (gui *Gui) handleCompleteMerge() error {
|
||||
if err := gui.stageSelectedFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
// if we got conflicts after unstashing, we don't want to call any git
|
||||
@@ -328,35 +290,35 @@ func (gui *Gui) handleCompleteMerge() error {
|
||||
}
|
||||
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
|
||||
if !gui.anyFilesWithMergeConflicts() {
|
||||
return gui.promptToContinue()
|
||||
return gui.promptToContinueRebase()
|
||||
}
|
||||
return gui.handleEscapeMerge()
|
||||
}
|
||||
|
||||
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
|
||||
func (gui *Gui) promptToContinue() error {
|
||||
gui.takeOverScrolling()
|
||||
// promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
|
||||
func (gui *Gui) promptToContinueRebase() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: "continue",
|
||||
prompt: gui.Tr.ConflictsResolved,
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.pushContext(gui.Contexts.Files.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Files); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.genericMergeCommand("continue")
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.pushContext(gui.Contexts.Files.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Files)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) canScrollMergePanel() bool {
|
||||
currentViewName := gui.currentViewName()
|
||||
if currentViewName != "main" {
|
||||
currentView := gui.g.CurrentView()
|
||||
if currentView != gui.Views.Main && currentView != gui.Views.Files {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -367,3 +329,14 @@ func (gui *Gui) canScrollMergePanel() bool {
|
||||
|
||||
return file.HasInlineMergeConflicts
|
||||
}
|
||||
|
||||
func (gui *Gui) withMergeConflictLock(f func() error) error {
|
||||
gui.State.Panels.Merging.ConflictsMutex.Lock()
|
||||
defer gui.State.Panels.Merging.ConflictsMutex.Unlock()
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
func (gui *Gui) takeOverMergeConflictScrolling() {
|
||||
gui.State.Panels.Merging.UserScrolling = false
|
||||
}
|
||||
|
||||
86
pkg/gui/mergeconflicts/merge_conflicts.go
Normal file
86
pkg/gui/mergeconflicts/merge_conflicts.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type Selection int
|
||||
|
||||
const (
|
||||
TOP Selection = iota
|
||||
BOTTOM
|
||||
BOTH
|
||||
)
|
||||
|
||||
func FindConflicts(content string) []commands.Conflict {
|
||||
conflicts := make([]commands.Conflict, 0)
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
|
||||
var newConflict commands.Conflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
switch trimmedLine {
|
||||
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
|
||||
newConflict = commands.Conflict{Start: i}
|
||||
case "=======":
|
||||
newConflict.Middle = i
|
||||
default:
|
||||
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
||||
newConflict.End = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
|
||||
if len(conflicts) == 0 {
|
||||
return content
|
||||
}
|
||||
conflict, remainingConflicts := shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := theme.DefaultTextColor
|
||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String()
|
||||
}
|
||||
|
||||
func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool {
|
||||
return i == conflict.Middle ||
|
||||
i == conflict.Start ||
|
||||
i == conflict.End ||
|
||||
selection != BOTH &&
|
||||
(selection == BOTTOM && i > conflict.Start && i < conflict.Middle) ||
|
||||
(selection == TOP && i > conflict.Middle && i < conflict.End)
|
||||
}
|
||||
|
||||
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
||||
}
|
||||
@@ -25,17 +25,6 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
||||
},
|
||||
reset: gui.exitDiffMode,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.Filtering.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%s '%s' %s", gui.Tr.LcFilteringBy, gui.State.Modes.Filtering.Path, utils.ColoredString(gui.Tr.ResetInParentheses, color.Underline)),
|
||||
color.FgRed,
|
||||
color.Bold,
|
||||
)
|
||||
},
|
||||
reset: gui.exitFilterMode,
|
||||
},
|
||||
{
|
||||
isActive: gui.GitCommand.PatchManager.Active,
|
||||
description: func() string {
|
||||
@@ -47,6 +36,17 @@ func (gui *Gui) modeStatuses() []modeStatus {
|
||||
},
|
||||
reset: gui.handleResetPatch,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.Filtering.Active,
|
||||
description: func() string {
|
||||
return utils.ColoredString(
|
||||
fmt.Sprintf("%s '%s' %s", gui.Tr.LcFilteringBy, gui.State.Modes.Filtering.GetPath(), utils.ColoredString(gui.Tr.ResetInParentheses, color.Underline)),
|
||||
color.FgRed,
|
||||
color.Bold,
|
||||
)
|
||||
},
|
||||
reset: gui.exitFilterMode,
|
||||
},
|
||||
{
|
||||
isActive: gui.State.Modes.CherryPicking.Active,
|
||||
description: func() string {
|
||||
|
||||
25
pkg/gui/modes/filtering/filtering.go
Normal file
25
pkg/gui/modes/filtering/filtering.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package filtering
|
||||
|
||||
type Filtering struct {
|
||||
path string // the filename that gets passed to git log
|
||||
}
|
||||
|
||||
func NewFiltering(path string) Filtering {
|
||||
return Filtering{path: path}
|
||||
}
|
||||
|
||||
func (m *Filtering) Active() bool {
|
||||
return m.path != ""
|
||||
}
|
||||
|
||||
func (m *Filtering) Reset() {
|
||||
m.path = ""
|
||||
}
|
||||
|
||||
func (m *Filtering) SetPath(path string) {
|
||||
m.path = path
|
||||
}
|
||||
|
||||
func (m *Filtering) GetPath() string {
|
||||
return m.path
|
||||
}
|
||||
@@ -45,8 +45,13 @@ func (gui *Gui) displayDescription(binding *Binding) string {
|
||||
return commandColor.Sprint(binding.Description)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
bindings := gui.getBindings(v)
|
||||
func (gui *Gui) handleCreateOptionsMenu() error {
|
||||
view := gui.g.CurrentView()
|
||||
if view == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindings := gui.getBindings(view)
|
||||
|
||||
menuItems := make([]*menuItem, len(bindings))
|
||||
|
||||
@@ -58,10 +63,10 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
if binding.Key == nil {
|
||||
return nil
|
||||
}
|
||||
if err := gui.handleMenuClose(g, v); err != nil {
|
||||
if err := gui.handleMenuClose(); err != nil {
|
||||
return err
|
||||
}
|
||||
return binding.Handler(g, v)
|
||||
return binding.Handler()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,25 +22,23 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *lBlPanelSt
|
||||
return gui.handleEscapePatchBuildingPanel()
|
||||
}
|
||||
|
||||
gui.splitMainPanel(true)
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
gui.Views.Main.Title = "Patch"
|
||||
gui.Views.Secondary.Title = "Custom Patch"
|
||||
|
||||
// get diff from commit file that's currently selected
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := commitFile.Parent
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, commitFile.Name, true)
|
||||
diff, err := gui.GitCommand.ShowFileDiff(from, to, reverse, node.GetPath(), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true)
|
||||
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(node.GetPath(), true, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,12 +76,12 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
|
||||
}
|
||||
|
||||
// add range of lines to those set for the file
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx); err != nil {
|
||||
if err := toggleFunc(node.GetPath(), state.FirstLineIdx, state.LastLineIdx); err != nil {
|
||||
// might actually want to return an error here
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
@@ -109,8 +107,8 @@ func (gui *Gui) handleEscapePatchBuildingPanel() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
}
|
||||
|
||||
if gui.currentContext().GetKey() == gui.Contexts.PatchBuilding.Context.GetKey() {
|
||||
return gui.pushContext(gui.Contexts.CommitFiles.Context)
|
||||
if gui.currentContext().GetKey() == gui.State.Contexts.PatchBuilding.GetKey() {
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
} else {
|
||||
// need to re-focus in case the secondary view should now be hidden
|
||||
return gui.currentContext().HandleFocus()
|
||||
@@ -125,7 +123,7 @@ func (gui *Gui) secondaryPatchPanelUpdateOpts() *viewUpdateOpts {
|
||||
title: "Custom Patch",
|
||||
noWrap: true,
|
||||
highlight: true,
|
||||
task: gui.createRenderStringWithoutScrollTask(patch),
|
||||
task: NewRenderStringWithoutScrollTask(patch),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreatePatchOptionsMenu() error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
return gui.createErrorPanel(gui.Tr.NoPatchError)
|
||||
}
|
||||
@@ -43,7 +42,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
},
|
||||
}...)
|
||||
|
||||
if gui.currentContext().GetKey() == gui.Contexts.BranchCommits.Context.GetKey() {
|
||||
if gui.currentContext().GetKey() == gui.State.Contexts.BranchCommits.GetKey() {
|
||||
selectedCommit := gui.getSelectedLocalCommit()
|
||||
if selectedCommit != nil && gui.GitCommand.PatchManager.To != selectedCommit.Sha {
|
||||
// adding this option to index 1
|
||||
@@ -180,7 +179,7 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
|
||||
func (gui *Gui) handleResetPatch() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
if gui.currentContextKeyIgnoringPopups() == MAIN_PATCH_BUILDING_CONTEXT_KEY {
|
||||
if err := gui.pushContext(gui.Contexts.CommitFiles.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.CommitFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,41 +8,31 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetCommitFileListDisplayStrings(commitFiles []*models.CommitFile, diffName string) [][]string {
|
||||
if len(commitFiles) == 0 {
|
||||
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
|
||||
}
|
||||
|
||||
lines := make([][]string, len(commitFiles))
|
||||
|
||||
for i := range commitFiles {
|
||||
diffed := commitFiles[i].Name == diffName
|
||||
lines[i] = getCommitFileDisplayStrings(commitFiles[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getCommitFileDisplayStrings returns the display string of branch
|
||||
func getCommitFileDisplayStrings(f *models.CommitFile, diffed bool) []string {
|
||||
func GetCommitFileLine(name string, diffName string, commitFile *models.CommitFile, status patch.PatchStatus) string {
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
diffTerminalColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
var colour *color.Color
|
||||
switch f.PatchStatus {
|
||||
case patch.UNSELECTED:
|
||||
colour = defaultColor
|
||||
case patch.WHOLE:
|
||||
colour = green
|
||||
case patch.PART:
|
||||
colour = yellow
|
||||
}
|
||||
if diffed {
|
||||
colour := defaultColor
|
||||
if diffName == name {
|
||||
colour = diffTerminalColor
|
||||
} else {
|
||||
switch status {
|
||||
case patch.UNSELECTED:
|
||||
colour = defaultColor
|
||||
case patch.WHOLE:
|
||||
colour = green
|
||||
case patch.PART:
|
||||
colour = yellow
|
||||
}
|
||||
}
|
||||
return []string{utils.ColoredString(f.ChangeStatus, getColorForChangeStatus(f.ChangeStatus)), colour.Sprint(f.Name)}
|
||||
|
||||
if commitFile == nil {
|
||||
return colour.Sprint(name)
|
||||
}
|
||||
|
||||
return utils.ColoredString(commitFile.ChangeStatus, getColorForChangeStatus(commitFile.ChangeStatus)) + " " + colour.Sprint(name)
|
||||
}
|
||||
|
||||
func getColorForChangeStatus(changeStatus string) color.Attribute {
|
||||
|
||||
@@ -7,57 +7,52 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetFileListDisplayStrings(files []*models.File, diffName string, submoduleConfigs []*models.SubmoduleConfig) [][]string {
|
||||
lines := make([][]string, len(files))
|
||||
|
||||
for i := range files {
|
||||
diffed := files[i].Name == diffName
|
||||
lines[i] = getFileDisplayStrings(files[i], diffed, submoduleConfigs)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getFileDisplayStrings returns the display string of branch
|
||||
func getFileDisplayStrings(f *models.File, diffed bool, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
func GetFileLine(hasUnstagedChanges bool, hasStagedChanges bool, name string, diffName string, submoduleConfigs []*models.SubmoduleConfig, file *models.File) string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
diffColor := color.New(theme.DiffTerminalColor)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
partiallyModifiedColor := color.New(color.FgYellow)
|
||||
|
||||
var restColor *color.Color
|
||||
if diffed {
|
||||
if name == diffName {
|
||||
restColor = diffColor
|
||||
} else if f.HasUnstagedChanges {
|
||||
} else if file == nil && hasStagedChanges && hasUnstagedChanges {
|
||||
restColor = partiallyModifiedColor
|
||||
} else if hasUnstagedChanges {
|
||||
restColor = red
|
||||
} else {
|
||||
restColor = green
|
||||
}
|
||||
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := f.DisplayString[0:1]
|
||||
firstCharCl := green
|
||||
if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
output := ""
|
||||
if file != nil {
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := file.ShortStatus[0:1]
|
||||
firstCharCl := green
|
||||
if firstChar == "?" {
|
||||
firstCharCl = red
|
||||
} else if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := file.ShortStatus[1:2]
|
||||
secondCharCl := red
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output = firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprint(" ")
|
||||
}
|
||||
|
||||
secondChar := f.DisplayString[1:2]
|
||||
secondCharCl := red
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
output += restColor.Sprint(name)
|
||||
|
||||
output := firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprintf(" %s", f.Name)
|
||||
|
||||
if f.IsSubmodule(submoduleConfigs) {
|
||||
if file != nil && file.IsSubmodule(submoduleConfigs) {
|
||||
output += utils.ColoredString(" (submodule)", theme.DefaultTextColor)
|
||||
}
|
||||
|
||||
return []string{output}
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) onResize() error {
|
||||
if gui.State.Ptmx == nil {
|
||||
return nil
|
||||
}
|
||||
mainView := gui.getMainView()
|
||||
width, height := mainView.Size()
|
||||
width, height := gui.Views.Main.Size()
|
||||
|
||||
if err := pty.Setsize(gui.State.Ptmx, &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}); err != nil {
|
||||
return err
|
||||
@@ -30,22 +30,17 @@ func (gui *Gui) onResize() error {
|
||||
// which is just an io.Reader. the pty package lets us wrap a command in a
|
||||
// pseudo-terminal meaning we'll get the behaviour we want from the underlying
|
||||
// command.
|
||||
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd, prefix string) error {
|
||||
width, _ := gui.getMainView().Size()
|
||||
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
|
||||
width, _ := gui.Views.Main.Size()
|
||||
pager := gui.GitCommand.GetPager(width)
|
||||
|
||||
if pager == "" {
|
||||
// if we're not using a custom pager we don't need to use a pty
|
||||
return gui.newCmdTask(viewName, cmd, prefix)
|
||||
return gui.newCmdTask(view, cmd, prefix)
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_PAGER="+pager)
|
||||
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // swallowing for now
|
||||
}
|
||||
|
||||
_, height := view.Size()
|
||||
_, oy := view.Origin()
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
package gui
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) onResize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd, prefix string) error {
|
||||
return gui.newCmdTask(viewName, cmd, prefix)
|
||||
func (gui *Gui) newPtyTask(view *gocui.View, cmd *exec.Cmd, prefix string) error {
|
||||
return gui.newCmdTask(view, cmd, prefix)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (gui *Gui) recordCurrentDirectory() error {
|
||||
return gui.OSCommand.CreateFileWithContent(os.Getenv("LAZYGIT_NEW_DIR_FILE"), dirName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleQuitWithoutChangingDirectory(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleQuitWithoutChangingDirectory() error {
|
||||
gui.State.RetainOriginalDir = true
|
||||
return gui.quit()
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (gui *Gui) handleQuit() error {
|
||||
return gui.quit()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleTopLevelReturn() error {
|
||||
currentContext := gui.currentContext()
|
||||
|
||||
parentContext, hasParent := currentContext.GetParentContext()
|
||||
@@ -49,15 +49,15 @@ func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
}
|
||||
|
||||
repoPathStack := gui.State.RepoPathStack
|
||||
repoPathStack := gui.RepoPathStack
|
||||
if len(repoPathStack) > 0 {
|
||||
n := len(repoPathStack) - 1
|
||||
|
||||
path := repoPathStack[n]
|
||||
|
||||
gui.State.RepoPathStack = repoPathStack[:n]
|
||||
gui.RepoPathStack = repoPathStack[:n]
|
||||
|
||||
return gui.dispatchSwitchToRepo(path)
|
||||
return gui.dispatchSwitchToRepo(path, true)
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().QuitOnTopLevelReturn {
|
||||
|
||||
@@ -50,8 +50,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
|
||||
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
|
||||
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspense(sub)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -68,8 +67,6 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
}
|
||||
if result == nil {
|
||||
return nil
|
||||
} else if result == gui.Errors.ErrSubProcess {
|
||||
return result
|
||||
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
|
||||
return gui.genericMergeCommand("skip")
|
||||
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
|
||||
@@ -83,7 +80,7 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
prompt: gui.Tr.FoundConflicts,
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushContext(gui.Contexts.Files.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Files)
|
||||
},
|
||||
handleClose: func() error {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -24,7 +25,10 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
|
||||
yellow.Sprint(path),
|
||||
},
|
||||
onPress: func() error {
|
||||
return gui.dispatchSwitchToRepo(path)
|
||||
// if we were in a submodule, we want to forget about that stack of repos
|
||||
// so that hitting escape in the new repo does nothing
|
||||
gui.RepoPathStack = []string{}
|
||||
return gui.dispatchSwitchToRepo(path, false)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -36,7 +40,7 @@ func (gui *Gui) handleShowAllBranchLogs() error {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.Config.GetUserConfig().Git.AllBranchesLogCmd,
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -46,18 +50,49 @@ func (gui *Gui) handleShowAllBranchLogs() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) dispatchSwitchToRepo(path string) error {
|
||||
func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
|
||||
env.UnsetGitDirEnvs()
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.Chdir(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return gui.createErrorPanel(gui.Tr.ErrRepositoryMovedOrDeleted)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := commands.VerifyInGitRepo(gui.OSCommand); err != nil {
|
||||
if err := os.Chdir(originalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.GitCommand = newGitCommand
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// these two mutexes are used by our background goroutines (triggered via `gui.goEvery`. We don't want to
|
||||
// switch to a repo while one of these goroutines is in the process of updating something
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
gui.Mutexes.RefreshingFilesMutex.Lock()
|
||||
defer gui.Mutexes.RefreshingFilesMutex.Unlock()
|
||||
|
||||
gui.resetState("", reuse)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func recordingEvents() bool {
|
||||
@@ -20,82 +18,29 @@ func recordEventsTo() string {
|
||||
return os.Getenv("RECORD_EVENTS_TO")
|
||||
}
|
||||
|
||||
func (gui *Gui) timeSinceStart() int64 {
|
||||
return time.Since(gui.StartTime).Nanoseconds() / 1e6
|
||||
func replaying() bool {
|
||||
return os.Getenv("REPLAY_EVENTS_FROM") != ""
|
||||
}
|
||||
|
||||
func (gui *Gui) replayRecordedEvents() {
|
||||
if os.Getenv("REPLAY_EVENTS_FROM") == "" {
|
||||
return
|
||||
}
|
||||
func headless() bool {
|
||||
return os.Getenv("HEADLESS") != ""
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
log.Fatal("20 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
|
||||
events, err := gui.loadRecordedEvents()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
// might need to add leeway if this ends up flakey
|
||||
var leeway int64 = 0
|
||||
func getRecordingSpeed() float64 {
|
||||
// humans are slow so this speeds things up.
|
||||
speed := 1
|
||||
envReplaySpeed := os.Getenv("REPLAY_SPEED")
|
||||
speed := 1.0
|
||||
envReplaySpeed := os.Getenv("SPEED")
|
||||
if envReplaySpeed != "" {
|
||||
var err error
|
||||
speed, err = strconv.Atoi(envReplaySpeed)
|
||||
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// The playback could be paused at any time because integration tests run concurrently.
|
||||
// Therefore we can't just check for a given event whether we've passed its timestamp,
|
||||
// or else we'll have an explosion of keypresses after the test is resumed.
|
||||
// We need to check if we've waited long enough since the last event was replayed.
|
||||
for i, event := range events {
|
||||
var prevEventTimestamp int64 = 0
|
||||
if i > 0 {
|
||||
prevEventTimestamp = events[i-1].Timestamp
|
||||
}
|
||||
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(speed)
|
||||
if i == 0 {
|
||||
timeToWait += leeway
|
||||
}
|
||||
var timeWaited int64 = 0
|
||||
middle:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
timeWaited += 1
|
||||
if gui.g != nil && timeWaited >= timeToWait {
|
||||
gui.g.ReplayedEvents <- *event.Event
|
||||
break middle
|
||||
}
|
||||
case <-gui.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
log.Fatal("lazygit should have already exited")
|
||||
return speed
|
||||
}
|
||||
|
||||
func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) {
|
||||
func (gui *Gui) loadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
@@ -103,22 +48,22 @@ func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []RecordedEvent{}
|
||||
recording := &gocui.Recording{}
|
||||
|
||||
err = json.Unmarshal(data, &events)
|
||||
err = json.Unmarshal(data, &recording)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
return recording, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) saveRecordedEvents() error {
|
||||
func (gui *Gui) saveRecording(recording *gocui.Recording) error {
|
||||
if !recordingEvents() {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonEvents, err := json.Marshal(gui.RecordedEvents)
|
||||
jsonEvents, err := json.Marshal(recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,14 +72,3 @@ func (gui *Gui) saveRecordedEvents() error {
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0600)
|
||||
}
|
||||
|
||||
func (gui *Gui) recordEvents() {
|
||||
for event := range gui.g.RecordedEvents {
|
||||
recordedEvent := RecordedEvent{
|
||||
Timestamp: gui.timeSinceStart(),
|
||||
Event: event,
|
||||
}
|
||||
|
||||
gui.RecordedEvents = append(gui.RecordedEvents, recordedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
@@ -21,13 +20,13 @@ func (gui *Gui) handleReflogCommitSelect() error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
var task updateTask
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask("No reflog history")
|
||||
task = NewRenderStringTask("No reflog history")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -73,17 +72,17 @@ func (gui *Gui) refreshReflogCommits() error {
|
||||
}
|
||||
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.Path); err != nil {
|
||||
if err := refresh(&state.FilteredReflogCommits, state.Modes.Filtering.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
state.FilteredReflogCommits = state.ReflogCommits
|
||||
}
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.ReflogCommits.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.ReflogCommits)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckoutReflogCommit() error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
@@ -105,7 +104,7 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateReflogResetMenu() error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
@@ -117,5 +116,5 @@ func (gui *Gui) handleViewReflogCommitFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.ReflogCommits.Context, "commits")
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.ReflogCommits, "commits")
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -23,12 +22,12 @@ func (gui *Gui) handleRemoteBranchSelect() error {
|
||||
var task updateTask
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch == nil {
|
||||
task = gui.createRenderStringTask("No branches for this remote")
|
||||
task = NewRenderStringTask("No branches for this remote")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
|
||||
)
|
||||
task = gui.createRunCommandTask(cmd)
|
||||
task = NewRunCommandTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -39,16 +38,16 @@ func (gui *Gui) handleRemoteBranchSelect() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteBranchesEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushContext(gui.Contexts.Remotes.Context)
|
||||
func (gui *Gui) handleRemoteBranchesEscape() error {
|
||||
return gui.pushContext(gui.State.Contexts.Remotes)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMergeRemoteBranch() error {
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleDeleteRemoteBranch() error {
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch == nil {
|
||||
return nil
|
||||
@@ -63,18 +62,18 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRebaseOntoRemoteBranch() error {
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
|
||||
return gui.handleRebaseOntoBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleSetBranchUpstream() error {
|
||||
selectedBranch := gui.getSelectedRemoteBranch()
|
||||
checkedOutBranch := gui.getCheckedOutBranch()
|
||||
|
||||
@@ -94,12 +93,12 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateResetToRemoteBranchMenu() error {
|
||||
selectedBranch := gui.getSelectedRemoteBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -25,9 +24,9 @@ func (gui *Gui) handleRemoteSelect() error {
|
||||
var task updateTask
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
task = gui.createRenderStringTask("No remotes")
|
||||
task = NewRenderStringTask("No remotes")
|
||||
} else {
|
||||
task = gui.createRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
task = NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -58,12 +57,7 @@ func (gui *Gui) refreshRemotes() error {
|
||||
}
|
||||
}
|
||||
|
||||
branchesView := gui.getBranchesView()
|
||||
if branchesView != nil {
|
||||
return gui.postRefreshUpdate(gui.mustContextForContextKey(branchesView.Context))
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.postRefreshUpdate(gui.mustContextForContextKey(ContextKey(gui.Views.Branches.Context)))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoteEnter() error {
|
||||
@@ -81,10 +75,10 @@ func (gui *Gui) handleRemoteEnter() error {
|
||||
}
|
||||
gui.State.Panels.RemoteBranches.SelectedLineIdx = newSelectedLine
|
||||
|
||||
return gui.pushContext(gui.Contexts.Remotes.Branches.Context)
|
||||
return gui.pushContext(gui.State.Contexts.RemoteBranches)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleAddRemote() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcNewRemoteName,
|
||||
handleConfirm: func(remoteName string) error {
|
||||
@@ -94,7 +88,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}})
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -102,7 +96,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRemoveRemote() error {
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return nil
|
||||
@@ -116,12 +110,12 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEditRemote() error {
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return nil
|
||||
@@ -164,14 +158,14 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFetchRemote() error {
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return nil
|
||||
@@ -184,6 +178,6 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
err := gui.GitCommand.FetchRemote(remote.Name, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ func (gui *Gui) resetToRef(ref string, strength string, options oscommands.RunCo
|
||||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
|
||||
if err := gui.pushContext(gui.Contexts.BranchCommits.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,26 +3,31 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleOpenSearch(viewName string) error {
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.Searching.isSearching = true
|
||||
gui.State.Searching.view = v
|
||||
gui.State.Searching.view = view
|
||||
|
||||
gui.renderString("search", "")
|
||||
gui.renderString(gui.Views.Search, "")
|
||||
|
||||
if err := gui.pushContext(gui.Contexts.Search.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Search); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Searching.searchString = gui.getSearchView().Buffer()
|
||||
func (gui *Gui) handleSearch() error {
|
||||
gui.State.Searching.searchString = gui.Views.Search.Buffer()
|
||||
gui.Log.Warn(gui.State.Searching.searchString)
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,7 +50,7 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
|
||||
return func(y int, index int, total int) error {
|
||||
if total == 0 {
|
||||
gui.renderString(
|
||||
"search",
|
||||
gui.Views.Search,
|
||||
fmt.Sprintf(
|
||||
"no matches for '%s' %s",
|
||||
gui.State.Searching.searchString,
|
||||
@@ -58,7 +63,7 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in
|
||||
return nil
|
||||
}
|
||||
gui.renderString(
|
||||
"search",
|
||||
gui.Views.Search,
|
||||
fmt.Sprintf(
|
||||
"matches for '%s' (%d of %d) %s",
|
||||
gui.State.Searching.searchString,
|
||||
@@ -92,7 +97,7 @@ func (gui *Gui) onSearchEscape() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleSearchEscape() error {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func (gui *Gui) nextSideWindow() error {
|
||||
windows := gui.getCyclableWindows()
|
||||
currentWindow := gui.currentWindow()
|
||||
@@ -19,7 +17,7 @@ func (gui *Gui) nextSideWindow() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -45,7 +43,7 @@ func (gui *Gui) previousSideWindow() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,8 +52,8 @@ func (gui *Gui) previousSideWindow() error {
|
||||
return gui.pushContextWithView(viewName)
|
||||
}
|
||||
|
||||
func (gui *Gui) goToSideWindow(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) goToSideWindow(sideViewName string) func() error {
|
||||
return func() error {
|
||||
return gui.pushContextWithView(sideViewName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gui
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
@@ -27,11 +26,11 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
}
|
||||
|
||||
if secondaryFocused {
|
||||
gui.getMainView().Title = gui.Tr.StagedChanges
|
||||
gui.getSecondaryView().Title = gui.Tr.UnstagedChanges
|
||||
gui.Views.Main.Title = gui.Tr.StagedChanges
|
||||
gui.Views.Secondary.Title = gui.Tr.UnstagedChanges
|
||||
} else {
|
||||
gui.getMainView().Title = gui.Tr.UnstagedChanges
|
||||
gui.getSecondaryView().Title = gui.Tr.StagedChanges
|
||||
gui.Views.Main.Title = gui.Tr.UnstagedChanges
|
||||
gui.Views.Secondary.Title = gui.Tr.StagedChanges
|
||||
}
|
||||
|
||||
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
|
||||
@@ -60,11 +59,11 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTogglePanelClick(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleTogglePanelClick() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
|
||||
return gui.refreshStagingPanel(false, v.SelectedLineIdx(), state)
|
||||
return gui.refreshStagingPanel(false, gui.Views.Secondary.SelectedLineIdx(), state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,7 +84,7 @@ func (gui *Gui) handleTogglePanel() error {
|
||||
func (gui *Gui) handleStagingEscape() error {
|
||||
gui.escapeLineByLinePanel()
|
||||
|
||||
return gui.pushContext(gui.Contexts.Files.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Files)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleStagedSelection() error {
|
||||
@@ -108,7 +107,7 @@ func (gui *Gui) handleResetSelection() error {
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if err := gui.pushContext(gui.Contexts.Staging.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Staging); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ func (gui *Gui) handleResetSelection() error {
|
||||
})
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.pushContext(gui.Contexts.Staging.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Staging)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -152,7 +151,7 @@ func (gui *Gui) applySelection(reverse bool, state *lBlPanelState) error {
|
||||
state.SelectMode = LINE
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(false, -1, state); err != nil {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -21,12 +20,12 @@ func (gui *Gui) handleStashEntrySelect() error {
|
||||
var task updateTask
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.NoStashEntries)
|
||||
task = NewRenderStringTask(gui.Tr.NoStashEntries)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
|
||||
)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -38,14 +37,14 @@ func (gui *Gui) handleStashEntrySelect() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStashEntries() error {
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.Path)
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.GetPath())
|
||||
|
||||
return gui.Contexts.Stash.Context.HandleRender()
|
||||
return gui.State.Contexts.Stash.HandleRender()
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashApply() error {
|
||||
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
|
||||
|
||||
apply := func() error {
|
||||
@@ -65,7 +64,7 @@ func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashPop() error {
|
||||
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
|
||||
|
||||
pop := func() error {
|
||||
@@ -85,7 +84,7 @@ func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashDrop() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.StashDrop,
|
||||
prompt: gui.Tr.SureDropStashEntry,
|
||||
@@ -110,7 +109,7 @@ func (gui *Gui) stashDo(method string) error {
|
||||
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
|
||||
@@ -124,7 +123,7 @@ func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
|
||||
if err := stashFunc(stashComment); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -135,5 +134,5 @@ func (gui *Gui) handleViewStashFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.Contexts.Stash.Context, "stash")
|
||||
return gui.switchToCommitFilesContext(stashEntry.RefName(), false, gui.State.Contexts.Stash, "stash")
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (gui *Gui) refreshStatus() {
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.getStatusView(), status)
|
||||
gui.setViewContent(gui.Views.Status, status)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -56,12 +56,12 @@ func cursorInSubstring(cx int, prefix string, substring string) bool {
|
||||
return cx >= runeCount(prefix) && cx < runeCount(prefix+substring)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckForUpdate() error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
|
||||
return gui.createLoaderPanel(gui.Tr.CheckingForUpdates)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStatusClick() error {
|
||||
// TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives)
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
@@ -73,11 +73,11 @@ func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.Contexts.Status.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
cx, _ := gui.Views.Status.Cursor()
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
switch gui.GitCommand.WorkingTreeState() {
|
||||
@@ -121,16 +121,16 @@ func (gui *Gui) handleStatusSelect() error {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(dashboardString),
|
||||
task: NewRenderStringTask(dashboardString),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleOpenConfig() error {
|
||||
return gui.openFile(gui.Config.GetUserConfigPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEditConfig() error {
|
||||
filename := gui.Config.GetUserConfigPath()
|
||||
return gui.editFile(filename)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
@@ -22,13 +21,13 @@ func (gui *Gui) handleSubCommitSelect() error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
var task updateTask
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask("No commits")
|
||||
task = NewRenderStringTask("No commits")
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -39,7 +38,7 @@ func (gui *Gui) handleSubCommitSelect() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutSubCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckoutSubCommit() error {
|
||||
commit := gui.getSelectedSubCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
@@ -73,7 +72,7 @@ func (gui *Gui) handleViewSubCommitFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.Contexts.SubCommits.Context, "branches")
|
||||
return gui.switchToCommitFilesContext(commit.Sha, false, gui.State.Contexts.SubCommits, "branches")
|
||||
}
|
||||
|
||||
func (gui *Gui) switchToSubCommitsContext(refName string) error {
|
||||
@@ -83,7 +82,7 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.Path,
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: false,
|
||||
RefName: refName,
|
||||
},
|
||||
@@ -95,13 +94,13 @@ func (gui *Gui) switchToSubCommitsContext(refName string) error {
|
||||
gui.State.SubCommits = commits
|
||||
gui.State.Panels.SubCommits.refName = refName
|
||||
gui.State.Panels.SubCommits.SelectedLineIdx = 0
|
||||
gui.Contexts.SubCommits.Context.SetParentContext(gui.currentSideContext())
|
||||
gui.State.Contexts.SubCommits.SetParentContext(gui.currentSideListContext())
|
||||
|
||||
return gui.pushContext(gui.Contexts.SubCommits.Context)
|
||||
return gui.pushContext(gui.State.Contexts.SubCommits)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToSubCommits() error {
|
||||
currentContext := gui.currentSideContext()
|
||||
currentContext := gui.currentSideListContext()
|
||||
if currentContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -25,7 +24,7 @@ func (gui *Gui) handleSubmoduleSelect() error {
|
||||
var task updateTask
|
||||
submodule := gui.getSelectedSubmodule()
|
||||
if submodule == nil {
|
||||
task = gui.createRenderStringTask("No submodules")
|
||||
task = NewRenderStringTask("No submodules")
|
||||
} else {
|
||||
prefix := fmt.Sprintf(
|
||||
"Name: %s\nPath: %s\nUrl: %s\n\n",
|
||||
@@ -36,11 +35,11 @@ func (gui *Gui) handleSubmoduleSelect() error {
|
||||
|
||||
file := gui.fileForSubmodule(submodule)
|
||||
if file == nil {
|
||||
task = gui.createRenderStringTask(prefix)
|
||||
task = NewRenderStringTask(prefix)
|
||||
} else {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
task = gui.createRunCommandTaskWithPrefix(cmd, prefix)
|
||||
task = NewRunCommandTaskWithPrefix(cmd, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +71,9 @@ func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.RepoPathStack = append(gui.State.RepoPathStack, wd)
|
||||
gui.RepoPathStack = append(gui.RepoPathStack, wd)
|
||||
|
||||
return gui.dispatchSwitchToRepo(submodule.Path)
|
||||
return gui.dispatchSwitchToRepo(submodule.Path, true)
|
||||
}
|
||||
|
||||
func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
@@ -86,7 +85,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES, FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES, FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -98,7 +97,7 @@ func (gui *Gui) handleResetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) {
|
||||
return file
|
||||
}
|
||||
@@ -110,7 +109,7 @@ func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File
|
||||
func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
file := gui.fileForSubmodule(submodule)
|
||||
if file != nil {
|
||||
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
@@ -122,7 +121,7 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES, SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES, SUBMODULES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddSubmodule() error {
|
||||
@@ -144,7 +143,7 @@ func (gui *Gui) handleAddSubmodule() error {
|
||||
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -164,7 +163,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
|
||||
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -175,21 +174,19 @@ func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error {
|
||||
err := gui.GitCommand.SubmoduleInit(submodule.Path)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) forSubmodule(callback func(*models.SubmoduleConfig) error) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.wrappedHandler(
|
||||
func() error {
|
||||
submodule := gui.getSelectedSubmodule()
|
||||
if submodule == nil {
|
||||
return nil
|
||||
}
|
||||
func (gui *Gui) forSubmodule(callback func(*models.SubmoduleConfig) error) func() error {
|
||||
return func() error {
|
||||
submodule := gui.getSelectedSubmodule()
|
||||
if submodule == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return callback(submodule)
|
||||
},
|
||||
)
|
||||
return callback(submodule)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetRemoveSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
@@ -221,7 +218,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -233,7 +230,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -245,7 +242,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -257,7 +254,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -271,6 +268,6 @@ func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
err := gui.GitCommand.SubmoduleUpdate(submodule.Path)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{SUBMODULES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,13 +24,8 @@ func (gui *Gui) getSelectedSuggestion() *types.Suggestion {
|
||||
}
|
||||
|
||||
func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) {
|
||||
view := gui.getSuggestionsView()
|
||||
if view == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gui.State.Suggestions = suggestions
|
||||
gui.State.Panels.Suggestions.SelectedLineIdx = 0
|
||||
_ = gui.resetOrigin(view)
|
||||
_ = gui.Contexts.Suggestions.Context.HandleRender()
|
||||
_ = gui.resetOrigin(gui.Views.Suggestions)
|
||||
_ = gui.State.Contexts.Suggestions.HandleRender()
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user