Compare commits
422 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eeff1257b | ||
|
|
c878f34ff1 | ||
|
|
f8db3592e3 | ||
|
|
d073932cec | ||
|
|
a2f7fcd730 | ||
|
|
f96674b24b | ||
|
|
a553f7fb77 | ||
|
|
6c415d1341 | ||
|
|
617e8a05ee | ||
|
|
b21ac990ea | ||
|
|
0740409f43 | ||
|
|
37700908cc | ||
|
|
488c43aaa2 | ||
|
|
bb4fe2653b | ||
|
|
a2ee52142c | ||
|
|
66d735acb5 | ||
|
|
d51b065f2a | ||
|
|
a3a14e9ff4 | ||
|
|
e58376f9f7 | ||
|
|
e8e4fa5957 | ||
|
|
b5d8849c06 | ||
|
|
5d1a9639b6 | ||
|
|
ea136e4e77 | ||
|
|
dcd3b7c058 | ||
|
|
fd8cb6e6d7 | ||
|
|
906ec30cac | ||
|
|
46c146a8c1 | ||
|
|
a8ec044f0e | ||
|
|
d626bcac00 | ||
|
|
123d624141 | ||
|
|
e798aa4b15 | ||
|
|
04e474aa66 | ||
|
|
0662733ad9 | ||
|
|
3c78ba7ed3 | ||
|
|
550c0fd4dc | ||
|
|
0bc0e4ac88 | ||
|
|
117c0bd4f7 | ||
|
|
79848087bc | ||
|
|
a3b820fb5f | ||
|
|
de5133ff90 | ||
|
|
1183de151a | ||
|
|
bfc9881213 | ||
|
|
3db40a79fe | ||
|
|
62393cf28a | ||
|
|
ec82f8099c | ||
|
|
b81bac3d65 | ||
|
|
58ddbae4d1 | ||
|
|
3802b563b0 | ||
|
|
d1134daa53 | ||
|
|
63cb304a82 | ||
|
|
6e579dc6e4 | ||
|
|
d5ec0fdcd1 | ||
|
|
0a63f701e5 | ||
|
|
bccf203a18 | ||
|
|
b590397dce | ||
|
|
755cc9f8d8 | ||
|
|
0e6598adbd | ||
|
|
f2645da16a | ||
|
|
f8f596d097 | ||
|
|
028cb2be2f | ||
|
|
fb69bfd20d | ||
|
|
f4874bbb74 | ||
|
|
eec20b845d | ||
|
|
3a0a9ec33b | ||
|
|
9b57b73f41 | ||
|
|
4fca89bc52 | ||
|
|
fc76b44b45 | ||
|
|
9a087d04eb | ||
|
|
c005b0d92b | ||
|
|
713fae3e32 | ||
|
|
148bf2c070 | ||
|
|
edfb0a26b2 | ||
|
|
f70435a20f | ||
|
|
b92ff3ee3f | ||
|
|
f1ced5539a | ||
|
|
77e9ee64a4 | ||
|
|
9daa47fb2d | ||
|
|
d18c8c8dc3 | ||
|
|
1573a449f8 | ||
|
|
7b19c5ad95 | ||
|
|
b363b75534 | ||
|
|
fc066d2f2e | ||
|
|
53ea7df655 | ||
|
|
533817bda3 | ||
|
|
35f1ccdb1b | ||
|
|
3dc3174d85 | ||
|
|
ae2496cf80 | ||
|
|
2ac33bb83d | ||
|
|
2b4048ebff | ||
|
|
31bcd632c7 | ||
|
|
aa9ef12d43 | ||
|
|
b80fafef02 | ||
|
|
130480555f | ||
|
|
92cc6e883d | ||
|
|
107503c903 | ||
|
|
7ae106d4df | ||
|
|
16dcc8f4db | ||
|
|
eb10ddfccc | ||
|
|
22a6771e51 | ||
|
|
3f96537380 | ||
|
|
a9f04d3925 | ||
|
|
83834a2c2e | ||
|
|
0c3132c6f0 | ||
|
|
b28569a593 | ||
|
|
1aa45b0142 | ||
|
|
39c8577074 | ||
|
|
23285eab40 | ||
|
|
0c2d90a444 | ||
|
|
d65c018875 | ||
|
|
0c135515a5 | ||
|
|
2b9df0ea06 | ||
|
|
b7b30191f1 | ||
|
|
7d1b76a349 | ||
|
|
40f10c3388 | ||
|
|
01e4467d76 | ||
|
|
b4e6850f98 | ||
|
|
c57a0077d0 | ||
|
|
46e500dc28 | ||
|
|
d7865b3882 | ||
|
|
0aad68acf0 | ||
|
|
4969e9ce0a | ||
|
|
17770b9f9b | ||
|
|
3dd88d6138 | ||
|
|
ce7cbe58a0 | ||
|
|
7588d5290b | ||
|
|
9fdf92b226 | ||
|
|
93bf691fd6 | ||
|
|
82022615dd | ||
|
|
fb395bca6e | ||
|
|
f91adf026b | ||
|
|
6d91661d5e | ||
|
|
90983aae65 | ||
|
|
f71b23b890 | ||
|
|
05a23f0e1e | ||
|
|
fd38ad8096 | ||
|
|
d502c43ae8 | ||
|
|
0df02dacc2 | ||
|
|
3258c24fb3 | ||
|
|
e7c657fba0 | ||
|
|
60468d2e17 | ||
|
|
cb78cf7de4 | ||
|
|
94b52af661 | ||
|
|
472288c81b | ||
|
|
258eedb38c | ||
|
|
bc044c64b2 | ||
|
|
e478c254d4 | ||
|
|
44f7fc6f7c | ||
|
|
a13e919d3d | ||
|
|
f92fcfbb47 | ||
|
|
6ccf58c224 | ||
|
|
9190e9beac | ||
|
|
a99e6ba071 | ||
|
|
604ee02cd9 | ||
|
|
926a48a65b | ||
|
|
98375dc902 | ||
|
|
e73de332a1 | ||
|
|
b28b2d05bd | ||
|
|
9e5f031553 | ||
|
|
dab5ba363c | ||
|
|
e42387d0da | ||
|
|
730a03a3b2 | ||
|
|
7d195b97c2 | ||
|
|
4fb2dba587 | ||
|
|
76697280c9 | ||
|
|
0df6ac6140 | ||
|
|
5453b71fd1 | ||
|
|
1f3070c882 | ||
|
|
3b7e7a7f56 | ||
|
|
06a8eb115c | ||
|
|
e4f0a470e9 | ||
|
|
adee0b8ccb | ||
|
|
0bebfe454e | ||
|
|
84b0c3df4f | ||
|
|
764bd556f3 | ||
|
|
069c7c9d35 | ||
|
|
393ce05860 | ||
|
|
cf78b86cb5 | ||
|
|
4f03d7733a | ||
|
|
e3a14d546a | ||
|
|
f2007f4d95 | ||
|
|
8969464b00 | ||
|
|
6137d66914 | ||
|
|
6fbe660f96 | ||
|
|
74320f0075 | ||
|
|
bfad972f0c | ||
|
|
bb918b579a | ||
|
|
e145090046 | ||
|
|
70b5c822bb | ||
|
|
f2df77a4f1 | ||
|
|
8d416634ba | ||
|
|
9f4433d8b5 | ||
|
|
2d8f7d2a7b | ||
|
|
a9fbc9eda1 | ||
|
|
e092da5f78 | ||
|
|
e42e7e5cbd | ||
|
|
93fac1f312 | ||
|
|
d5504fa5d0 | ||
|
|
273aba38d4 | ||
|
|
cab0aa462c | ||
|
|
b03e2270a0 | ||
|
|
21049be233 | ||
|
|
f89c47b83d | ||
|
|
44f1f22068 | ||
|
|
a229547048 | ||
|
|
4f700c23ba | ||
|
|
b69fc19b35 | ||
|
|
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 |
23
.github/workflows/cd.yml
vendored
23
.github/workflows/cd.yml
vendored
@@ -29,26 +29,3 @@ jobs:
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
ppa:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout PPA repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dawidd6/lazygit-debian
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Setup git
|
||||
uses: dawidd6/action-git-user-config@v1
|
||||
- name: Update PPA repo
|
||||
run: |
|
||||
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
|
||||
sudo apt update
|
||||
sudo apt install -y git-buildpackage
|
||||
git fetch --tags https://github.com/$GITHUB_REPOSITORY
|
||||
gbp import-ref -u "$version"
|
||||
gbp dch -D xenial -N "$version"-1
|
||||
git add debian/changelog
|
||||
git commit -m "d/changelog: dch $version"
|
||||
gbp tag
|
||||
git push --tags origin master
|
||||
|
||||
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@@ -22,19 +22,37 @@ jobs:
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
- name: Test code
|
||||
run: |
|
||||
./test.sh
|
||||
- name: Build binaries
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
args: --skip-publish --snapshot
|
||||
go-version: 1.16.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Build linux binary
|
||||
run: |
|
||||
GOOS=linux go build
|
||||
- name: Build windows binary
|
||||
run: |
|
||||
GOOS=windows go build
|
||||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
|
||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -12,3 +12,9 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
|
||||
15
README.md
15
README.md
@@ -21,7 +21,6 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
@@ -82,6 +81,8 @@ sudo port install lazygit
|
||||
|
||||
### Ubuntu
|
||||
|
||||
**Deprecated**: will no longer receive updates.
|
||||
|
||||
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||
|
||||
```sh
|
||||
@@ -114,12 +115,12 @@ scoop install lazygit
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
|
||||
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
- Stable: <https://aur.archlinux.org/packages/lazygit/>
|
||||
- Stable: `sudo pacman -S lazygit`
|
||||
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
|
||||
|
||||
Instruction of how to install AUR content can be found here:
|
||||
@@ -279,12 +280,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
|
||||
|
||||
|
||||
530
docs/Config.md
530
docs/Config.md
@@ -2,181 +2,201 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
- Linux: `~/.config/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\lazygit\config.yml`
|
||||
|
||||
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
|
||||
|
||||
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
## Default
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
allBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
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
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scrool up
|
||||
scrollDownMain: '<pgdown>' # main panel scrool down
|
||||
scrollUpMain-alt1: 'K' # main panel scrool up
|
||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
showRandomTip: true
|
||||
showCommandLog: true
|
||||
commandLogSize: 8
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'auto' # one of 'auto' | 'merge' | 'rebase' | 'ff-only', auto reads from git configuration
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
parseEmoji: false
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
openCommand: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
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: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scroll up
|
||||
scrollDownMain: '<pgdown>' # main panel scroll down
|
||||
scrollUpMain-alt1: 'K' # main panel scroll up
|
||||
scrollDownMain-alt1: 'J' # main panel scroll down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scroll up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scroll down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
viewPullRequestOptions: 'O'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
```
|
||||
|
||||
## Platform Defaults
|
||||
@@ -184,31 +204,50 @@ Default path for the config file:
|
||||
### Windows
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Configuring File Editing
|
||||
|
||||
Lazygit will edit a file with the first set editor in the following:
|
||||
|
||||
1. config.yaml
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim' # as an example
|
||||
```
|
||||
|
||||
2. \$(git config core.editor)
|
||||
3. \$GIT_EDITOR
|
||||
4. \$VISUAL
|
||||
5. \$EDITOR
|
||||
6. \$(which vi)
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
@@ -216,7 +255,8 @@ for users of VSCode
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
The available attributes are:
|
||||
|
||||
- default
|
||||
**Colors**
|
||||
|
||||
- black
|
||||
- red
|
||||
- green
|
||||
@@ -225,7 +265,12 @@ The available attributes are:
|
||||
- magenta
|
||||
- cyan
|
||||
- white
|
||||
- '#ff00ff'
|
||||
|
||||
**Modifiers**
|
||||
|
||||
- bold
|
||||
- default
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
@@ -234,31 +279,50 @@ The available attributes are:
|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## 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:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- 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
|
||||
|
||||

|
||||
@@ -270,33 +334,33 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
@@ -307,7 +371,7 @@ the pull request. You can do so on your `config.yml` file using the following sy
|
||||
|
||||
```yaml
|
||||
services:
|
||||
"<gitDomain>": "<provider>:<webDomain>"
|
||||
'<gitDomain>': '<provider>:<webDomain>'
|
||||
```
|
||||
|
||||
Where:
|
||||
@@ -317,19 +381,21 @@ Where:
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
- Branch name: feature/AB-123
|
||||
- Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: "[$1] "
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: '[$1] '
|
||||
```
|
||||
|
||||
## Custom git log command
|
||||
|
||||
@@ -35,6 +35,20 @@ customCommands:
|
||||
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
|
||||
context: 'localBranches'
|
||||
loadingText: 'creating branch'
|
||||
- key : 'r'
|
||||
description: 'Checkout a remote branch as FETCH_HEAD'
|
||||
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
|
||||
context: 'remotes'
|
||||
prompts:
|
||||
- type: 'input'
|
||||
title: 'Remote:'
|
||||
initialValue: "{{index .SelectedRemote.Name }}"
|
||||
- type: 'menuFromCommand'
|
||||
title: 'Remote branch:'
|
||||
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
|
||||
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
|
||||
valueFormat: '{{ .branch }}'
|
||||
labelFormat: '{{ .branch | green }}'
|
||||
```
|
||||
|
||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||
@@ -79,12 +93,28 @@ The permitted contexts are:
|
||||
|
||||
The permitted prompt fields are:
|
||||
|
||||
| _field_ | _description_ | _required_ |
|
||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||
| type | one of 'input' or 'menu' | yes |
|
||||
| title | the title to display in the popup panel | no |
|
||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||
| _field_ | _description_ | _required_ |
|
||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||
| type | one of 'input' or 'menu' | yes |
|
||||
| title | the title to display in the popup panel | no |
|
||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
||||
| | menu options | |
|
||||
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
|
||||
| | groups which are going to be kept from the command's output | |
|
||||
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
|
||||
| | the filter to construct a menu item's value (What gets appended to prompt | |
|
||||
| | responses when the item is selected). You can use named groups, | |
|
||||
| | or `{{ .group_GROUPID }}`. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
|
||||
| | the filter to construct the item's label (What's shown on screen). You can use | |
|
||||
| | named groups, or `{{ .group_GROUPID }}`. You can also color each match with | |
|
||||
| | `{{ .group_GROUPID | colorname }}` (Color names from | |
|
||||
| | [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)) | |
|
||||
| | If `labelFormat` is not specified, `valueFormat` is shown instead. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
|
||||
The permitted option fields are:
|
||||
| _field_ | _description_ | _required_ |
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||

|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Global Keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
@@ -16,9 +17,10 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
@@ -38,6 +40,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
@@ -109,7 +112,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)
|
||||
@@ -121,7 +125,7 @@
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
@@ -153,6 +157,12 @@
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Files Panel (Files)
|
||||
|
||||
<pre>
|
||||
@@ -170,10 +180,13 @@
|
||||
<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
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
@@ -193,6 +206,7 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
@@ -205,8 +219,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)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Globaale Sneltoetsen
|
||||
## Globale Sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
@@ -13,31 +14,33 @@
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
|
||||
<kbd>_</kbd>: vorige schermmode
|
||||
<kbd>:</kbd>: voor aangepast commando uit
|
||||
<kbd>|</kbd>: bekijk scoping opties
|
||||
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>:</kbd>: voor aangepaste commando uit
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
## Lijstpaneel Navigatie
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
<kbd>/</kbd>: start met zoeken
|
||||
<kbd>]</kbd>: volgende tabblad
|
||||
<kbd>[</kbd>: vorige tabblad
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Branches Tab)
|
||||
## Branches Paneel (Branches Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak een pull-aanvraag
|
||||
<kbd>o</kbd>: maak een pull-request
|
||||
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
@@ -49,16 +52,16 @@
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
||||
## Branches Paneel (Remote Branches (in Remotes tabblad))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
@@ -67,7 +70,7 @@
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remotes Tab)
|
||||
## Branches Paneel (Remotes Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
@@ -83,13 +86,13 @@
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Tags Tab)
|
||||
## Branches Paneel (Tags Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
@@ -97,7 +100,7 @@
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Paneel
|
||||
@@ -109,7 +112,8 @@
|
||||
<kbd>o</kbd>: open bestand
|
||||
<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>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
@@ -129,28 +133,34 @@
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tab)
|
||||
## Commits Paneel (Reflog Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
</pre>
|
||||
|
||||
## Extras Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Bestanden)
|
||||
@@ -174,25 +184,29 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: view reset and remove submodule options
|
||||
<kbd>d</kbd>: bekijk reset en verwijder submodule opties
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>n</kbd>: voeg nieuwe submodule toe
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd>i</kbd>: initialiseer submodule
|
||||
<kbd>b</kbd>: bekijk bulk submodule opties
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Merggen)
|
||||
## Hoofd Paneel (Mergen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
@@ -202,29 +216,29 @@
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Normaal)
|
||||
## Hoofd 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)
|
||||
## Hoofd Paneel (Patch Bouwen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Staging)
|
||||
## Hoofd Paneel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
@@ -238,9 +252,9 @@
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
@@ -255,7 +269,7 @@
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>enter</kbd>: bekijk bestanden van stash entry
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
@@ -269,5 +283,5 @@
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle takken van het houtblok laten zien
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
</pre>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
@@ -16,9 +17,10 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
@@ -38,6 +40,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
||||
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
@@ -109,7 +112,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)
|
||||
@@ -121,7 +125,7 @@
|
||||
<kbd>g</kbd>: zresetuj do tego commita
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commits (autosquash)
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
@@ -153,6 +157,12 @@
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Pliki)
|
||||
|
||||
<pre>
|
||||
@@ -174,6 +184,9 @@
|
||||
<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
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
@@ -193,6 +206,7 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
@@ -205,8 +219,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)
|
||||
|
||||
22
go.mod
22
go.mod
@@ -9,36 +9,40 @@ require (
|
||||
github.com/cli/safeexec v1.0.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/gdamore/tcell/v2 v2.3.11 // indirect
|
||||
github.com/go-errors/errors v1.4.0
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210614081440-74b42ecad52b
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/kyokomi/emoji/v2 v2.2.8
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.10
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/nsf/termbox-go v0.0.0-20210114135735-d04385b850e8 // indirect
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 // indirect
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
66
go.sum
66
go.sum
@@ -23,7 +23,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886 h1:NAFoy+QgUpERgK3y1xiVh5HcOvSeZHpXTTo5qnvnuK4=
|
||||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
@@ -31,17 +30,22 @@ 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/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.3.11 h1:ECO6WqHGbKZ3HrSL7bG/zArMCmLaNr5vcjjMVnLHpzc=
|
||||
github.com/gdamore/tcell/v2 v2.3.11/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/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.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM=
|
||||
github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
@@ -53,9 +57,10 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
@@ -65,14 +70,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210614081440-74b42ecad52b h1:Wc2zx6xKLNaHc7/wIO6iYyDSjcFGN1Osd6tQvbgMmgo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210614081440-74b42ecad52b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -85,38 +84,38 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
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=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
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.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-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.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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=
|
||||
@@ -126,7 +125,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -142,21 +140,22 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
|
||||
@@ -168,20 +167,25 @@ 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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
|
||||
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/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.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=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -195,3 +199,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
3
main.go
3
main.go
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
@@ -134,6 +135,6 @@ func main() {
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
|
||||
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,15 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -12,17 +21,6 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// App struct
|
||||
@@ -97,6 +95,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 +181,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 +224,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ func (app *App) Run() error {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := app.Gui.RunWithSubprocesses()
|
||||
err := app.Gui.RunAndHandleError()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -316,6 +316,9 @@ func TailLogs() {
|
||||
|
||||
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
||||
|
||||
opts := humanlog.DefaultOptions
|
||||
opts.Truncates = false
|
||||
|
||||
_, err = os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -324,22 +327,5 @@ func TailLogs() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := humanlog.DefaultOptions
|
||||
opts.Truncates = false
|
||||
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
TailLogsForPlatform(logFilePath, opts)
|
||||
}
|
||||
|
||||
29
pkg/app/logging.go
Normal file
29
pkg/app/logging.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// +build !windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
71
pkg/app/logging_windows.go
Normal file
71
pkg/app/logging_windows.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// +build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
var lastModified int64 = 0
|
||||
var lastOffset int64 = 0
|
||||
for {
|
||||
stat, err := os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if stat.ModTime().Unix() > lastModified {
|
||||
err = TailFrom(lastOffset, logFilePath, opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
lastOffset = stat.Size()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func OpenAndSeek(filepath string, offset int64) (*os.File, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = file.Seek(offset, 0)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func TailFrom(lastOffset int64, logFilePath string, opts *humanlog.HandlerOptions) error {
|
||||
file, err := OpenAndSeek(logFilePath, lastOffset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
var lines []string
|
||||
for fileScanner.Scan() {
|
||||
lines = append(lines, fileScanner.Text())
|
||||
}
|
||||
file.Close()
|
||||
lineCount := len(lines)
|
||||
lastTen := lines
|
||||
if lineCount > 10 {
|
||||
lastTen = lines[lineCount-10:]
|
||||
}
|
||||
for _, line := range lastTen {
|
||||
reader := strings.NewReader(line)
|
||||
if err := humanlog.Scanner(reader, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -59,9 +59,9 @@ type CheckoutOptions struct {
|
||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if options.Force {
|
||||
forceArg = "--force "
|
||||
forceArg = " --force"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
@@ -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,28 @@ 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) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + 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)
|
||||
}
|
||||
|
||||
338
pkg/commands/branches_test.go
Normal file
338
pkg/commands/branches_test.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetCommitDifferences is a function.
|
||||
func TestGitCommandGetCommitDifferences(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Can't retrieve pushable count",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Can't retrieve pullable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("test")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Retrieve pullable and pushable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("echo", "10")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo", "11")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "11", pushableCount)
|
||||
assert.EqualValues(t, "10", pullableCount)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandNewBranch is a function.
|
||||
func TestGitCommandNewBranch(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test", "master"))
|
||||
}
|
||||
|
||||
// TestGitCommandDeleteBranch is a function.
|
||||
func TestGitCommandDeleteBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch string
|
||||
force bool
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Delete a branch",
|
||||
"test",
|
||||
false,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Force delete a branch",
|
||||
"test",
|
||||
true,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DeleteBranch(s.branch, s.force))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandMerge is a function.
|
||||
func TestGitCommandMerge(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCheckout is a function.
|
||||
func TestGitCommandCheckout(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
force bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Checkout",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Checkout forced",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetBranchGraph is a function.
|
||||
func TestGitCommandGetBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
_, err := gitCmd.GetBranchGraph("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitCommandGetAllBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
|
||||
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandCurrentBranchName is a function.
|
||||
func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"says we are on the master branch if we are",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("echo", "master")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falls back to git `git branch --contains` if symbolic-ref fails",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"handles a detached head",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123abcd", name)
|
||||
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", name)
|
||||
assert.EqualValues(t, "", displayname)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CurrentBranchName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandResetHard is a function.
|
||||
func TestGitCommandResetHard(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
ref string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"HEAD",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git reset --hard HEAD`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.ResetHard(s.ref))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -12,7 +10,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
|
||||
@@ -20,20 +18,19 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options oscomman
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||
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)
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
flagsStr := ""
|
||||
if flags != "" {
|
||||
flagsStr = fmt.Sprintf(" %s", flags)
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
@@ -50,19 +47,17 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
|
||||
}
|
||||
|
||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
}
|
||||
|
||||
func (c *GitCommand) AmendHeadCmdStr() string {
|
||||
return "git commit --amend --no-edit --allow-empty"
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
@@ -75,7 +70,11 @@ 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)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
|
||||
return c.RunCommand("git revert %s -m %d", sha, parentNumber)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
111
pkg/commands/commits_test.go
Normal file
111
pkg/commands/commits_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRenameCommit is a function.
|
||||
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", "--only", "-m", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.RenameCommit("test"))
|
||||
}
|
||||
|
||||
// TestGitCommandResetToCommit is a function.
|
||||
func TestGitCommandResetToCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
flags string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
flags: "",
|
||||
expected: "git commit -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "test",
|
||||
flags: "--no-verify",
|
||||
expected: "git commit --no-verify -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
flags: "",
|
||||
expected: "git commit -m \"line1\" -m \"line2\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCreateFixupCommit is a function.
|
||||
func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"12345",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git commit --fixup=12345`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CreateFixupCommit(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 ""
|
||||
}
|
||||
@@ -46,3 +46,17 @@ func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
|
||||
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) UsingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
70
pkg/commands/config_test.go
Normal file
70
pkg/commands/config_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
func TestGitCommandUsingGpg(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(bool)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
func(string) (string, error) { return "", nil },
|
||||
func(gpgEnabled bool) {
|
||||
assert.False(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
func(string) (string, error) {
|
||||
return "True", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
func(string) (string, error) {
|
||||
return "ON", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
func(string) (string, error) {
|
||||
return "YeS", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
func(string) (string, error) {
|
||||
return "1", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.UsingGpg())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,5 @@ func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitComman
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
removeFile: func(string) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,48 +2,53 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os"
|
||||
"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)
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||
return "git mergetool"
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeTool() error {
|
||||
return c.OSCommand.RunCommand("git mergetool")
|
||||
}
|
||||
|
||||
// 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 +63,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 +111,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 {
|
||||
return c.removeFile(file.Name)
|
||||
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.Added {
|
||||
return c.OSCommand.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
|
||||
@@ -133,29 +189,32 @@ func (c *GitCommand) Ignore(filename string) error {
|
||||
}
|
||||
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
|
||||
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, ignoreWhitespace 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())
|
||||
ignoreWhitespaceArg := ""
|
||||
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"
|
||||
}
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceArg = "--ignore-all-space"
|
||||
}
|
||||
|
||||
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 %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
@@ -170,7 +229,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
|
||||
@@ -191,12 +250,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// 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, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
@@ -206,7 +265,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
|
||||
}
|
||||
@@ -218,10 +277,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
err := c.AmendHead()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -232,17 +288,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
|
||||
@@ -265,11 +321,16 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GetConfigValue("core.editor")
|
||||
}
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("VISUAL")
|
||||
}
|
||||
@@ -282,10 +343,8 @@ 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 "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
|
||||
|
||||
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
}
|
||||
|
||||
894
pkg/commands/files_test.go
Normal file
894
pkg/commands/files_test.go
Normal file
@@ -0,0 +1,894 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
func TestGitCommandCatFile(t *testing.T) {
|
||||
var osCmd string
|
||||
switch os := runtime.GOOS; os {
|
||||
case "windows":
|
||||
osCmd = "type"
|
||||
default:
|
||||
osCmd = "cat"
|
||||
}
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo", "-n", "test")
|
||||
}
|
||||
|
||||
o, err := gitCmd.CatFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", o)
|
||||
}
|
||||
|
||||
// TestGitCommandStageFile is a function.
|
||||
func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StageFile("test.txt"))
|
||||
}
|
||||
|
||||
// TestGitCommandUnstageFile is a function.
|
||||
func TestGitCommandUnstageFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
reset bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"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)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"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)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
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
|
||||
command func() (func(string, ...string) *exec.Cmd, *[][]string)
|
||||
test func(*[][]string, error)
|
||||
file *models.File
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred when resetting",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when removing file",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "an error occurred when removing file")
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
},
|
||||
func(string) error {
|
||||
return fmt.Errorf("an error occurred when removing file")
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred with checkout",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Checkout only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout staged changes",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout merge conflicts",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and remove",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Remove only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
var cmdsCalled *[][]string
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command, cmdsCalled = s.command()
|
||||
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
|
||||
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiff is a function.
|
||||
func TestGitCommandDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *models.File
|
||||
plain bool
|
||||
cached bool
|
||||
ignoreWhitespace bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Default case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"plain",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"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)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: false,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Default case (ignore whitespace)",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--ignore-all-space", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCheckoutFile is a function.
|
||||
func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"typical case",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 -- test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error if there is one",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 -- test999.txt",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("echo", "done")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"command returns error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
// TODO: Ideally we want to mock out OSCommand here so that we're not
|
||||
// double handling testing it's CreateTempFile functionality,
|
||||
// but it is going to take a bit of work to make a proper mock for it
|
||||
// so I'm leaving it for another PR
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.ApplyPatch("test", "cached"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
},
|
||||
0,
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git cat-file -e HEAD^:test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git checkout HEAD^ -- test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git commit --amend --no-edit --allow-empty",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git rebase --continue",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
// test for when the file was created within the commit requires a refactor to support proper mocks
|
||||
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
file *models.File
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
&models.File{Name: "test.txt"},
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- "test.txt"`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAnyUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- .`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandRemoveUntrackedFiles is a function.
|
||||
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git clean -fd`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.RemoveUntrackedFiles())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEditFileCmdStr is a function.
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
configEditCommand string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"nano",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "emacs \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
"",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"file/with space\"", cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
@@ -32,7 +33,6 @@ type GitCommand struct {
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGitConfigValue func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
@@ -54,10 +54,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
|
||||
}
|
||||
@@ -78,7 +74,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
@@ -88,8 +83,25 @@ 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 (c *GitCommand) WithSpan(span string) *GitCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newGitCommand := &GitCommand{}
|
||||
*newGitCommand = *c
|
||||
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
|
||||
|
||||
// NOTE: unlike the other things here which create shallow clones, this will
|
||||
// actually update the PatchManager on the original struct to have the new span.
|
||||
// This means each time we call ApplyPatch in PatchManager, we need to ensure
|
||||
// we've called .WithSpan() ahead of time with the new span value
|
||||
newGitCommand.PatchManager.ApplyPatch = newGitCommand.ApplyPatch
|
||||
|
||||
return newGitCommand
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
@@ -120,6 +132,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 +213,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package commands
|
||||
|
||||
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict bars appear
|
||||
type Conflict struct {
|
||||
Start int
|
||||
Middle int
|
||||
End int
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,12 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
|
||||
}
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
if len(split) != 4 {
|
||||
// Ignore line if it isn't separated into 4 parts
|
||||
// This is probably a warning message, for more info see:
|
||||
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -72,10 +72,6 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
|
||||
// Any commit with multiple parents is a merge commit.
|
||||
// If there's a space then it means there must be more than one parent hash
|
||||
isMerge := strings.Contains(parentHashes, " ")
|
||||
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
@@ -83,7 +79,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
IsMerge: isMerge,
|
||||
Parents: strings.Split(parentHashes, " "),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +165,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,90 +22,95 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*models.File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
if strings.HasPrefix(statusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", statusString)
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := statusString[0:2]
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
unstagedChange := change[1:2]
|
||||
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)
|
||||
|
||||
file := &models.File{
|
||||
Name: filename,
|
||||
DisplayString: statusString,
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.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(status.Name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
// GitStatus returns the file status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
type FileStatus struct {
|
||||
StatusString string
|
||||
Change string // ??, MM, AM, ...
|
||||
Name string
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
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 []FileStatus{}, err
|
||||
}
|
||||
|
||||
appendedIndexes := []int{}
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
response := []FileStatus{}
|
||||
|
||||
// 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)
|
||||
for i := 0; i < len(splitLines); i++ {
|
||||
original := splitLines[i]
|
||||
|
||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
}
|
||||
if len(original) < 3 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// append any new files to the end
|
||||
for index, newFile := range newFiles {
|
||||
if !utils.IncludesInt(appendedIndexes, index) {
|
||||
result = append(result, newFile)
|
||||
status := FileStatus{
|
||||
StatusString: original,
|
||||
Change: original[:2],
|
||||
Name: original[3:],
|
||||
PreviousName: "",
|
||||
}
|
||||
|
||||
if strings.HasPrefix(status.Change, "R") {
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
status.PreviousName = strings.TrimSpace(splitLines[i+1])
|
||||
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
|
||||
i++
|
||||
}
|
||||
|
||||
response = append(response, status)
|
||||
}
|
||||
|
||||
return result
|
||||
return response, nil
|
||||
}
|
||||
|
||||
227
pkg/commands/loading_files_test.go
Normal file
227
pkg/commands/loading_files_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStatusFiles is a function.
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 5)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`MM a\nb.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM a\nb.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 2)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "after1.txt",
|
||||
PreviousName: "before1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "R before1.txt -> after1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "R ",
|
||||
},
|
||||
{
|
||||
Name: "after2.txt",
|
||||
PreviousName: "before2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "RM before2.txt -> after2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`?? a -> b.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a -> b.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? a -> b.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
61
pkg/commands/loading_stash_test.go
Normal file
61
pkg/commands/loading_stash_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStashEntries is a function.
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.StashEntry)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
assert.Len(t, entries, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
expected := []*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, entries, 2)
|
||||
assert.EqualValues(t, expected, entries)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
|
||||
|
||||
func convertToInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -37,52 +25,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
|
||||
tags[i] = &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
// now lets sort our tags by name numerically
|
||||
re := regexp.MustCompile(semverRegex)
|
||||
|
||||
// the reason this is complicated is because we're both sorting alphabetically
|
||||
// and when we're dealing with semver strings
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
a := tags[i].Name
|
||||
b := tags[j].Name
|
||||
|
||||
matchA := re.FindStringSubmatch(a)
|
||||
matchB := re.FindStringSubmatch(b)
|
||||
|
||||
if len(matchA) > 0 && len(matchB) > 0 {
|
||||
numbersA := strings.Split(matchA[1], ".")
|
||||
numbersB := strings.Split(matchB[1], ".")
|
||||
k := 0
|
||||
for {
|
||||
if len(numbersA) == k && len(numbersB) == k {
|
||||
break
|
||||
}
|
||||
if len(numbersA) == k {
|
||||
return true
|
||||
}
|
||||
if len(numbersB) == k {
|
||||
return false
|
||||
}
|
||||
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
|
||||
return true
|
||||
}
|
||||
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
|
||||
return false
|
||||
}
|
||||
k++
|
||||
}
|
||||
|
||||
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
|
||||
}
|
||||
|
||||
return strings.ToLower(a) < strings.ToLower(b)
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
@@ -24,3 +24,26 @@ func (b *Branch) ID() string {
|
||||
func (b *Branch) Description() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
||||
// this method does not consider the case where the git config states that a branch is tracking the config.
|
||||
// The Pullables value here is based on whether or not we saw an upstream when doing `git branch`
|
||||
func (b *Branch) IsTrackingRemote() bool {
|
||||
return b.IsRealBranch() && b.Pullables != "?"
|
||||
}
|
||||
|
||||
func (b *Branch) MatchesUpstream() bool {
|
||||
return b.IsRealBranch() && b.Pushables == "0" && b.Pullables == "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPush() bool {
|
||||
return b.IsRealBranch() && b.Pushables != "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPull() bool {
|
||||
return b.IsRealBranch() && b.Pullables != "0"
|
||||
}
|
||||
|
||||
// for when we're in a detached head state
|
||||
func (b *Branch) IsRealBranch() bool {
|
||||
return b.Pushables != "" && b.Pullables != ""
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ type Commit struct {
|
||||
Author string
|
||||
UnixTimestamp int64
|
||||
|
||||
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
|
||||
IsMerge bool
|
||||
// SHAs of parent commits (will be multiple if it's a merge commit)
|
||||
Parents []string
|
||||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
@@ -35,3 +35,7 @@ func (c *Commit) ID() string {
|
||||
func (c *Commit) Description() string {
|
||||
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
|
||||
}
|
||||
|
||||
func (c *Commit) IsMerge() bool {
|
||||
return len(c.Parents) > 1
|
||||
}
|
||||
|
||||
@@ -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,25 @@ type File struct {
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
@@ -51,10 +61,27 @@ func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
|
||||
|
||||
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||
for _, config := range configs {
|
||||
if f.Name == config.Name {
|
||||
if f.Name == config.Path {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -42,6 +40,44 @@ type OSCommand struct {
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
Getenv func(string) string
|
||||
|
||||
// callback to run before running a command, i.e. for the purposes of logging
|
||||
onRunCommand func(CmdLogEntry)
|
||||
|
||||
// something like 'Staging File': allows us to group cmd logs under a single title
|
||||
CmdLogSpan string
|
||||
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
// TODO: make these fields private
|
||||
type CmdLogEntry struct {
|
||||
// e.g. 'git commit -m "haha"'
|
||||
cmdStr string
|
||||
// Span is something like 'Staging File'. Multiple commands can be grouped under the same
|
||||
// span
|
||||
span string
|
||||
|
||||
// sometimes our command is direct like 'git commit', and sometimes it's a
|
||||
// command to remove a file but through Go's standard library rather than the
|
||||
// command line
|
||||
commandLine bool
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCmdStr() string {
|
||||
return e.cmdStr
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetSpan() string {
|
||||
return e.span
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCommandLine() bool {
|
||||
return e.commandLine
|
||||
}
|
||||
|
||||
func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
|
||||
return CmdLogEntry{cmdStr: cmdStr, span: span, commandLine: commandLine}
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
@@ -53,15 +89,51 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
Command: secureexec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) WithSpan(span string) *OSCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newOSCommand := &OSCommand{}
|
||||
*newOSCommand = *c
|
||||
newOSCommand.CmdLogSpan = span
|
||||
return newOSCommand
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
|
||||
c.LogCommand(strings.Join(cmd.Args, " "), true)
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
|
||||
c.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
|
||||
if c.onRunCommand != nil && c.CmdLogSpan != "" {
|
||||
c.onRunCommand(NewCmdLogEntry(cmdStr, c.CmdLogSpan, commandLine))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
|
||||
c.onRunCommand = f
|
||||
}
|
||||
|
||||
// SetCommand sets the command function used by the struct.
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||
c.Command = cmd
|
||||
}
|
||||
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetRemoveFile(f func(string) error) {
|
||||
c.removeFile = f
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||
c.BeforeExecuteCmd = cmd
|
||||
}
|
||||
@@ -71,9 +143,12 @@ type RunCommandOptions struct {
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -93,17 +168,18 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
if formatArgs != nil {
|
||||
command = fmt.Sprintf(formatString, formatArgs...)
|
||||
}
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
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
|
||||
}
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
c.LogExecCmd(cmd)
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
@@ -129,7 +205,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)
|
||||
@@ -141,6 +217,18 @@ func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string)
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
@@ -174,6 +262,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 {
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, 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 +285,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 {
|
||||
@@ -212,10 +301,15 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
quoted := c.Quote(filename)
|
||||
if c.Platform.OS == "linux" {
|
||||
// Add extra quoting to avoid issues with shell command string
|
||||
quoted = c.Quote(quoted)
|
||||
quoted = quoted[1 : len(quoted)-1]
|
||||
}
|
||||
templateValues := map[string]string{
|
||||
"filename": quoted,
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
@@ -223,6 +317,7 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
|
||||
// OpenLink opens a file with the given
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
@@ -240,27 +335,33 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
c.LogExecCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
if strings.Contains(message, c.Platform.EscapedQuote) {
|
||||
escapedQuote = c.Platform.FallbackEscapedQuote
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
@@ -281,6 +382,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
@@ -296,6 +398,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
c.Log.Error(err)
|
||||
return err
|
||||
@@ -311,6 +414,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
|
||||
// Remove removes a file or directory at the specified path
|
||||
func (c *OSCommand) Remove(filename string) error {
|
||||
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
|
||||
err := os.RemoveAll(filename)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
@@ -331,6 +435,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
// before running it
|
||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
c.LogExecCmd(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
outString := string(out)
|
||||
c.Log.Info(outString)
|
||||
@@ -352,19 +457,18 @@ 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 {
|
||||
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
|
||||
logCmdStr := ""
|
||||
for i, str := range commandStrings {
|
||||
if i > 0 {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
}
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
@@ -455,5 +559,12 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
func (c *OSCommand) RemoveFile(path string) error {
|
||||
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
@@ -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}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Platform.OS = "darwin"
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
@@ -110,10 +111,86 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFile tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"test\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"filename with spaces\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"let's_test_with_single_quote\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "sh", name)
|
||||
assert.Equal(t, []string{"-c", "xdg-open \"\\$USER.txt\" > /dev/null"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "linux"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `sh -c "xdg-open {{filename}} > /dev/null"`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.OS = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
@@ -129,7 +206,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 +219,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
|
||||
}
|
||||
|
||||
@@ -176,11 +182,8 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
parser, err := NewPatchParser(p.Log, patch)
|
||||
if err != nil {
|
||||
// swallowing for now
|
||||
return ""
|
||||
}
|
||||
parser := NewPatchParser(p.Log, patch)
|
||||
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
@@ -216,7 +219,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
|
||||
|
||||
@@ -4,14 +4,16 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -37,11 +39,8 @@ type PatchParser struct {
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
|
||||
hunkStarts, stageableLines, patchLines := parsePatch(patch)
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
@@ -51,7 +50,7 @@ func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
@@ -96,55 +95,49 @@ func (l *PatchLine) render(selected bool, included bool) string {
|
||||
if l.Kind == HUNK_HEADER {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
match := re.FindStringSubmatch(content)
|
||||
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
}
|
||||
|
||||
var colorAttr color.Attribute
|
||||
textStyle := theme.DefaultTextColor
|
||||
switch l.Kind {
|
||||
case PATCH_HEADER:
|
||||
colorAttr = color.Bold
|
||||
textStyle = textStyle.SetBold()
|
||||
case ADDITION:
|
||||
colorAttr = color.FgGreen
|
||||
textStyle = style.FgGreen
|
||||
case DELETION:
|
||||
colorAttr = color.FgRed
|
||||
textStyle = style.FgRed
|
||||
case COMMIT_SHA:
|
||||
colorAttr = color.FgYellow
|
||||
default:
|
||||
colorAttr = theme.DefaultTextColor
|
||||
textStyle = style.FgYellow
|
||||
}
|
||||
|
||||
return coloredString(colorAttr, content, selected, included)
|
||||
return coloredString(textStyle, content, selected, included)
|
||||
}
|
||||
|
||||
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
|
||||
if selected {
|
||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
||||
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
|
||||
firstCharStyle := textStyle
|
||||
if included {
|
||||
clIncluded = color.New(append(attributes, color.BgGreen)...)
|
||||
} else {
|
||||
clIncluded = color.New(attributes...)
|
||||
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return utils.ColoredStringDirect(str, clIncluded)
|
||||
return firstCharStyle.Sprint(str)
|
||||
}
|
||||
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind int
|
||||
var lineKind PatchLineKind
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
@@ -183,7 +176,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines, nil
|
||||
return hunkStarts, stageableLines, patchLines
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
|
||||
@@ -23,7 +23,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd
|
||||
}
|
||||
|
||||
// time to amend the selected commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the source commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
@@ -158,7 +158,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,14 +5,62 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
PullRequestURL string
|
||||
Name string
|
||||
pullRequestURLIntoDefaultBranch func(owner string, repository string, from string) string
|
||||
pullRequestURLIntoTargetBranch func(owner string, repository string, from string, to string) string
|
||||
}
|
||||
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s?expand=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s...%s?expand=1", siteDomain, owner, repository, to, from)
|
||||
},
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&t=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&dest=%s&t=1", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s&merge_request[target_branch]=%s", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) PullRequestURL(owner string, repository string, from string, to string) string {
|
||||
if to == "" {
|
||||
return s.pullRequestURLIntoDefaultBranch(owner, repository, from)
|
||||
} else {
|
||||
return s.pullRequestURLIntoTargetBranch(owner, repository, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
@@ -28,31 +76,6 @@ type RepoInformation struct {
|
||||
Repository string
|
||||
}
|
||||
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func getServices(config config.AppConfigurer) []*Service {
|
||||
services := []*Service{
|
||||
NewService("github", "github.com", "github.com"),
|
||||
@@ -90,27 +113,27 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) error {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
func (pr *PullRequest) Create(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
func (pr *PullRequest) CopyURL(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||
@@ -131,9 +154,8 @@ func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error)
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
pullRequestURL := fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
)
|
||||
|
||||
pullRequestURL := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -48,18 +47,17 @@ func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *models.Branch
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
@@ -71,15 +69,14 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
@@ -91,15 +88,14 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
@@ -111,15 +107,74 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/ui",
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
@@ -131,20 +186,78 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/divide-operation",
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
@@ -167,7 +280,7 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
return s.remoteUrl, nil
|
||||
}
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
} else {
|
||||
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
@@ -117,7 +119,7 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI
|
||||
var commitAction string
|
||||
if i == actionIndex {
|
||||
commitAction = action
|
||||
} else if commit.IsMerge {
|
||||
} else if commit.IsMerge() {
|
||||
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
|
||||
// doing this means we don't need to worry about rebasing over merges which always causes problems.
|
||||
// you typically shouldn't be doing rebases that pass over merge commits anyway.
|
||||
@@ -211,7 +213,7 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
|
||||
96
pkg/commands/rebasing_test.go
Normal file
96
pkg/commands/rebasing_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRebaseBranch is a function.
|
||||
func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"successful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"unsuccessful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
|
||||
// environment variables that suppress an interactive editor
|
||||
func TestGitCommandSkipEditorCommand(t *testing.T) {
|
||||
cmd := NewDummyGitCommand()
|
||||
|
||||
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^VISUAL="),
|
||||
"expected VISUAL to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^EDITOR="),
|
||||
"expected EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^GIT_EDITOR="),
|
||||
"expected GIT_EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
|
||||
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
|
||||
)
|
||||
})
|
||||
|
||||
_ = cmd.runSkipEditorCommand("true")
|
||||
}
|
||||
@@ -2,24 +2,22 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -28,10 +26,10 @@ func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, pr
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
branchName,
|
||||
)
|
||||
|
||||
return err == nil
|
||||
|
||||
@@ -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
|
||||
@@ -21,8 +21,8 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// 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 {
|
||||
// wrap in 'writing', which uses a mutex
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
35
pkg/commands/stash_entries_test.go
Normal file
35
pkg/commands/stash_entries_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandStashDo is a function.
|
||||
func TestGitCommandStashDo(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashDo(1, "drop"))
|
||||
}
|
||||
|
||||
// TestGitCommandStashSave is a function.
|
||||
func TestGitCommandStashSave(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashSave("A stash message"))
|
||||
}
|
||||
@@ -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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
|
||||
@@ -2,23 +2,9 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
followTagsFlag := "--follow-tags"
|
||||
@@ -74,3 +60,32 @@ func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func
|
||||
command := fmt.Sprintf("git fetch %s", remoteName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPullMode(mode string) string {
|
||||
if mode != "auto" {
|
||||
return mode
|
||||
}
|
||||
|
||||
var isRebase bool
|
||||
var isFf bool
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
isRebase = c.GetConfigValue("pull.rebase") == "true"
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
isFf = c.GetConfigValue("pull.ff") == "only"
|
||||
wg.Done()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
if isRebase {
|
||||
return "rebase"
|
||||
} else if isFf {
|
||||
return "ff-only"
|
||||
} else {
|
||||
return "merge"
|
||||
}
|
||||
}
|
||||
|
||||
223
pkg/commands/sync_test.go
Normal file
223
pkg/commands/sync_test.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandPush is a function.
|
||||
func TestGitCommandPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
command func(string, ...string) *exec.Cmd
|
||||
forcePush bool
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Push with force disabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
true,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
|
||||
return "\n"
|
||||
})
|
||||
s.test(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type getPullModeScenario struct {
|
||||
testName string
|
||||
getGitConfigValueMock func(string) (string, error)
|
||||
configPullModeValue string
|
||||
test func(string)
|
||||
}
|
||||
|
||||
func TestGetPullMode(t *testing.T) {
|
||||
|
||||
scenarios := getPullModeScenarios(t)
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValueMock
|
||||
s.test(gitCmd.GetPullMode(s.configPullModeValue))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getPullModeScenarios(t *testing.T) []getPullModeScenario {
|
||||
return []getPullModeScenario{
|
||||
{
|
||||
testName: "Merge is default",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "merge", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when pull.rebase is true",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads ff-only when pull.ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when rebase is true and ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads rebase when pull.rebase is true",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.rebase" {
|
||||
return "true", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Reads ff-only when pull.ff is only",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
if s == "pull.ff" {
|
||||
return "only", nil
|
||||
}
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "auto",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects merge config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "merge",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "merge", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects rebase config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "rebase",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "rebase", actual)
|
||||
},
|
||||
}, {
|
||||
testName: "Respects ff-only config",
|
||||
getGitConfigValueMock: func(s string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
configPullModeValue: "ff-only",
|
||||
test: func(actual string) {
|
||||
assert.Equal(t, "ff-only", actual)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
|
||||
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `cmd /c "start "" {{filename}}"`,
|
||||
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ type GuiConfig struct {
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
ShowListFooter bool `yaml:"showListFooter"`
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||
CommandLogSize int `yaml:"commandLogSize"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
@@ -61,6 +66,7 @@ type GitConfig struct {
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
}
|
||||
|
||||
type PagingConfig struct {
|
||||
@@ -100,6 +106,7 @@ type KeybindingConfig struct {
|
||||
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
|
||||
}
|
||||
|
||||
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||
type KeybindingUniversalConfig struct {
|
||||
Quit string `yaml:"quit"`
|
||||
QuitAlt1 string `yaml:"quit-alt1"`
|
||||
@@ -118,6 +125,8 @@ type KeybindingUniversalConfig struct {
|
||||
NextBlock string `yaml:"nextBlock"`
|
||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
@@ -153,8 +162,11 @@ type KeybindingUniversalConfig struct {
|
||||
DiffingMenu string `yaml:"diffingMenu"`
|
||||
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -175,10 +187,13 @@ type KeybindingFilesConfig struct {
|
||||
ToggleStagedAll string `yaml:"toggleStagedAll"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
CreatePullRequest string `yaml:"createPullRequest"`
|
||||
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
|
||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||
@@ -237,6 +252,9 @@ type KeybindingSubmodulesConfig struct {
|
||||
|
||||
// OSConfig contains config on the level of the os
|
||||
type OSConfig struct {
|
||||
// EditCommand is the command for editing a file
|
||||
EditCommand string `yaml:"editCommand,omitempty"`
|
||||
|
||||
// OpenCommand is the command for opening a file
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
|
||||
@@ -263,6 +281,12 @@ type CustomCommandPrompt struct {
|
||||
|
||||
// this only applies to menus
|
||||
Options []CustomCommandMenuOption
|
||||
|
||||
// this only applies to menuFromCommand
|
||||
Command string `yaml:"command"`
|
||||
Filter string `yaml:"filter"`
|
||||
ValueFormat string `yaml:"valueFormat"`
|
||||
LabelFormat string `yaml:"labelFormat"`
|
||||
}
|
||||
|
||||
type CustomCommandMenuOption struct {
|
||||
@@ -292,6 +316,11 @@ func GetDefaultConfig() *UserConfig {
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
@@ -303,7 +332,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Args: "",
|
||||
},
|
||||
Pull: PullConfig{
|
||||
Mode: "merge",
|
||||
Mode: "auto",
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
@@ -311,6 +340,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||
DisableForcePushing: false,
|
||||
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
||||
ParseEmoji: false,
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
@@ -323,7 +353,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Reporting: "undetermined",
|
||||
SplashUpdatesIndex: 0,
|
||||
ConfirmOnQuit: false,
|
||||
QuitOnTopLevelReturn: true,
|
||||
QuitOnTopLevelReturn: false,
|
||||
Keybinding: KeybindingConfig{
|
||||
Universal: KeybindingUniversalConfig{
|
||||
Quit: "q",
|
||||
@@ -343,6 +373,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlock: "<right>",
|
||||
PrevBlockAlt: "h",
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -356,6 +388,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
OpenFile: "o",
|
||||
OpenRecentRepos: "<c-r>",
|
||||
ScrollUpMain: "<pgup>",
|
||||
ScrollDownMain: "<pgdown>",
|
||||
ScrollUpMainAlt1: "K",
|
||||
@@ -379,7 +412,9 @@ func GetDefaultConfig() *UserConfig {
|
||||
DiffingMenuAlt: "<c-e>",
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<tab>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
ToggleWhitespaceInDiffView: "<c-w>",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -398,10 +433,13 @@ func GetDefaultConfig() *UserConfig {
|
||||
ToggleStagedAll: "a",
|
||||
ViewResetOptions: "D",
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
OpenMergeTool: "M",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
CreatePullRequest: "o",
|
||||
ViewPullRequestOptions: "O",
|
||||
CheckoutBranchByName: "c",
|
||||
ForceCheckoutBranch: "F",
|
||||
RebaseBranch: "r",
|
||||
|
||||
35
pkg/constants/links.go
Normal file
35
pkg/constants/links.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package constants
|
||||
|
||||
type Docs struct {
|
||||
CustomPagers string
|
||||
CustomCommands string
|
||||
CustomKeybindings string
|
||||
Keybindings string
|
||||
Undoing string
|
||||
Config string
|
||||
Tutorial string
|
||||
}
|
||||
|
||||
var Links = struct {
|
||||
Docs Docs
|
||||
Issues string
|
||||
Donate string
|
||||
Discussions string
|
||||
RepoUrl string
|
||||
Releases string
|
||||
}{
|
||||
RepoUrl: "https://github.com/jesseduffield/lazygit",
|
||||
Issues: "https://github.com/jesseduffield/lazygit/issues",
|
||||
Donate: "https://github.com/sponsors/jesseduffield",
|
||||
Discussions: "https://github.com/jesseduffield/lazygit/discussions",
|
||||
Releases: "https://github.com/jesseduffield/lazygit/releases",
|
||||
Docs: Docs{
|
||||
CustomPagers: "https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md",
|
||||
CustomKeybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md",
|
||||
CustomCommands: "https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium",
|
||||
Keybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
|
||||
Undoing: "https://github.com/jesseduffield/lazygit/blob/master/docs/Undoing.md",
|
||||
Config: "https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
Tutorial: "https://youtu.be/VDXvbHZYeKY",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
@@ -130,6 +132,24 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
if !gui.ShowExtrasWindow {
|
||||
return 0
|
||||
}
|
||||
|
||||
var baseSize int
|
||||
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
baseSize = 1000 // my way of saying 'fill the available space'
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
return baseSize + frameSize
|
||||
}
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
width, height := gui.g.Size()
|
||||
|
||||
@@ -146,6 +166,8 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
mainPanelsDirection = boxlayout.COLUMN
|
||||
}
|
||||
|
||||
extrasWindowSize := gui.getExtrasWindowSize(height)
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
@@ -159,9 +181,19 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
ConditionalChildren: gui.sidePanelChildren,
|
||||
},
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Direction: boxlayout.ROW,
|
||||
Weight: mainSectionWeight,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "extras",
|
||||
Size: extrasWindowSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -181,9 +213,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 +313,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()
|
||||
|
||||
77
pkg/gui/basic_context.go
Normal file
77
pkg/gui/basic_context.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package gui
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
WindowName string
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetParentContext(context Context) {
|
||||
c.ParentContext = context
|
||||
c.hasParent = true
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetParentContext() (Context, bool) {
|
||||
return c.ParentContext, c.hasParent
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetWindowName(windowName string) {
|
||||
c.WindowName = windowName
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetWindowName() string {
|
||||
windowName := c.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKind() ContextKind {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -94,6 +96,11 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
||||
var boxSize int
|
||||
if child.isStatic() {
|
||||
boxSize = child.Size
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
if boxSize > availableSize {
|
||||
boxSize = availableSize
|
||||
}
|
||||
} else {
|
||||
// TODO: consider more evenly distributing the remainder
|
||||
boxSize = unitSize * child.Weight
|
||||
@@ -120,7 +127,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 {
|
||||
@@ -179,6 +179,27 @@ func TestArrangeWindows(t *testing.T) {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with static child with size too large",
|
||||
&Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 9},
|
||||
// not sure if X0: 10, X1: 9 makes any sense, but testing this in the
|
||||
// actual GUI it seems harmless
|
||||
"dynamic1": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
||||
@@ -4,9 +4,9 @@ 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/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -31,13 +31,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 +70,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 +79,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
|
||||
}
|
||||
@@ -87,46 +87,52 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutBranch})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreatePullRequestPress() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.createPullRequest(branch.Name, "")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestMenu() error {
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
}
|
||||
checkedOutBranch := gui.getCheckedOutBranch()
|
||||
|
||||
return gui.createPullRequestMenu(selectedBranch, checkedOutBranch)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.CopyURL(branch); err != nil {
|
||||
url, err := pullRequest.CopyURL(branch.Name, "")
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false))
|
||||
|
||||
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
|
||||
|
||||
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
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
err := gui.fetch(true)
|
||||
err := gui.fetch(true, "Fetch")
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
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
|
||||
@@ -135,7 +141,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.ForceCheckoutBranch).Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
@@ -147,6 +153,7 @@ type handleCheckoutRefOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
onRefNotFound func(ref string) error
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||
@@ -164,8 +171,10 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
}
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(options.span)
|
||||
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := gitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
|
||||
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
@@ -179,15 +188,15 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
title: gui.Tr.AutoStashTitle,
|
||||
prompt: gui.Tr.AutoStashPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
if err := gitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := gitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,12 +217,13 @@ 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,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
span: "Checkout branch",
|
||||
onRefNotFound: func(ref string) error {
|
||||
|
||||
return gui.ask(askOpts{
|
||||
@@ -251,7 +261,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)
|
||||
}
|
||||
|
||||
@@ -286,14 +296,14 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
return gui.deleteNamedBranch(selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(errMessage)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -322,13 +332,13 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
title: gui.Tr.MergingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Merge).Merge(branchName, commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 +347,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)
|
||||
}
|
||||
@@ -363,24 +373,22 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
title: gui.Tr.RebasingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.RebaseBranch(selectedBranchName)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.RebaseBranch).RebaseBranch(selectedBranchName)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFastForward() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
if branch == nil || !branch.IsRealBranch() {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "" {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "?" {
|
||||
|
||||
if !branch.IsTrackingRemote() {
|
||||
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
|
||||
}
|
||||
if branch.Pushables != "0" {
|
||||
if branch.HasCommitsToPush() {
|
||||
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
|
||||
}
|
||||
|
||||
@@ -389,6 +397,8 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.FastForwardBranch
|
||||
|
||||
split := strings.Split(upstream, "/")
|
||||
remoteName := split[0]
|
||||
remoteBranchName := strings.Join(split[1:], "/")
|
||||
@@ -404,17 +414,17 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
_ = gui.createLoaderPanel(message)
|
||||
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
|
||||
} else {
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(span).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,30 +433,35 @@ 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 {
|
||||
if branch == nil || !branch.IsRealBranch() {
|
||||
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 + ":",
|
||||
initialContent: branch.Name,
|
||||
handleConfirm: func(newBranchName string) error {
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
// need to checkout so that the branch shows up in our reflog and therefore
|
||||
// doesn't get lost among all the other branches when we switch to something else
|
||||
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RenameBranch).RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
// 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 nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -454,8 +469,7 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
// I could do an explicit check here for whether the branch is tracking a remote branch
|
||||
// but if we've selected it we'll already know that via Pullables and Pullables.
|
||||
// Bit of a hack but I'm lazy.
|
||||
notTrackingRemote := branch.Pullables == "?"
|
||||
if notTrackingRemote {
|
||||
if !branch.IsTrackingRemote() {
|
||||
return promptForNewName()
|
||||
}
|
||||
|
||||
@@ -474,7 +488,7 @@ func (gui *Gui) currentBranch() *models.Branch {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
@@ -490,15 +504,15 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
|
||||
prefilledName := ""
|
||||
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
||||
// will set to the remote's existing name
|
||||
prefilledName = item.ID()
|
||||
// will set to the remote's branch name without the remote name
|
||||
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: message,
|
||||
initialContent: prefilledName,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.NewBranch(sanitizedBranchName(response), item.ID()); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateBranch).NewBranch(sanitizedBranchName(response), item.ID()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -508,8 +522,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
|
||||
}
|
||||
}
|
||||
@@ -540,7 +554,7 @@ func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
|
||||
for i, branchName := range matchingBranchNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: branchName,
|
||||
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
|
||||
Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,5 +564,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)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
// you can only copy from one context at a time, because the order and position of commits matter
|
||||
|
||||
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
|
||||
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
|
||||
|
||||
if oldContextKey != context.GetKey() {
|
||||
// need to reset the cherry picking mode
|
||||
gui.State.Modes.CherryPicking.ContextKey = context.GetKey()
|
||||
gui.State.Modes.CherryPicking.ContextKey = string(context.GetKey())
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0)
|
||||
|
||||
return gui.rerenderContextViewIfPresent(oldContextKey)
|
||||
@@ -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
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
||||
prompt: gui.Tr.SureCherryPick,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.CherryPick).CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -156,7 +156,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) exitCherryPickingMode() error {
|
||||
contextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
contextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
|
||||
|
||||
gui.State.Modes.CherryPicking.ContextKey = ""
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = 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
|
||||
}
|
||||
|
||||
184
pkg/gui/command_log_panel.go
Normal file
184
pkg/gui/command_log_panel.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
// closing over this so that nobody else can modify it
|
||||
currentSpan := ""
|
||||
|
||||
return func(entry oscommands.CmdLogEntry) {
|
||||
if gui.Views.Extras == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
if entry.GetSpan() != currentSpan {
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(entry.GetSpan()))
|
||||
currentSpan = entry.GetSpan()
|
||||
}
|
||||
|
||||
textStyle := theme.DefaultTextColor
|
||||
if !entry.GetCommandLine() {
|
||||
textStyle = style.FgMagenta
|
||||
}
|
||||
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
|
||||
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) printCommandLogHeader() {
|
||||
introStr := fmt.Sprintf(
|
||||
gui.Tr.CommandLogHeader,
|
||||
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
|
||||
)
|
||||
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
|
||||
|
||||
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
|
||||
fmt.Fprintf(
|
||||
gui.Views.Extras,
|
||||
"%s: %s",
|
||||
style.FgYellow.Sprint(gui.Tr.RandomTip),
|
||||
style.FgGreen.Sprint(gui.getRandomTip()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getRandomTip() string {
|
||||
config := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
formattedKey := func(key string) string {
|
||||
return gui.getKeyDisplay(key)
|
||||
}
|
||||
|
||||
tips := []string{
|
||||
// keybindings and lazygit-specific advice
|
||||
fmt.Sprintf(
|
||||
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
|
||||
formattedKey(config.Universal.PushFiles),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To filter commits by path, press '%s'",
|
||||
formattedKey(config.Universal.FilteringMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To start an interactive rebase, press '%s' on a commit. You can always abort the rebase by pressing '%s' and selecting 'abort'",
|
||||
formattedKey(config.Universal.Edit),
|
||||
formattedKey(config.Universal.CreateRebaseOptionsMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"In flat file view, merge conflicts are sorted to the top. To switch to flat file view press '%s'",
|
||||
formattedKey(config.Files.ToggleTreeView),
|
||||
),
|
||||
"If you want to learn Go and can think of ways to improve lazygit, join the team! Click 'Ask Question' and express your interest",
|
||||
fmt.Sprintf(
|
||||
"If you press '%s'/'%s' you can undo/redo your changes. Be wary though, this only applies to branches/commits, so only do this if your worktree is clear.\nDocs: %s",
|
||||
formattedKey(config.Universal.Undo),
|
||||
formattedKey(config.Universal.Redo),
|
||||
constants.Links.Docs.Undoing,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"to hard reset onto your current upstream branch, press '%s' in the files panel",
|
||||
formattedKey(config.Files.ViewResetOptions),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To push a tag, navigate to the tag in the tags tab and press '%s'",
|
||||
formattedKey(config.Branches.PushTag),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can view the individual files of a stash entry by pressing '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can diff two commits by pressing '%s' on one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
|
||||
formattedKey(config.Universal.DiffingMenu),
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"press '%s' on a commit to drop it (delete it)",
|
||||
formattedKey(config.Universal.Remove),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'",
|
||||
formattedKey(config.Files.OpenMergeTool),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To revert a commit, press '%s' on that commit",
|
||||
formattedKey(config.Commits.RevertCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
|
||||
formattedKey(config.Universal.Return),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To search for a string in your panel, press '%s'",
|
||||
formattedKey(config.Universal.StartSearch),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can page through the items of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.PrevPage),
|
||||
formattedKey(config.Universal.NextPage),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can jump to the top/bottom of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.GotoTop),
|
||||
formattedKey(config.Universal.GotoBottom),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To collapse/expand a directory, press '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can append your staged changes to an older commit by pressing '%s' on that commit",
|
||||
formattedKey(config.Commits.AmendToCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can amend the last commit with your new file changes by pressing '%s' in the files panel",
|
||||
formattedKey(config.Files.AmendLastCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can now navigate the side panels with '%s' and '%s'",
|
||||
formattedKey(config.Universal.NextBlockAlt2),
|
||||
formattedKey(config.Universal.PrevBlockAlt2),
|
||||
),
|
||||
|
||||
"You can use lazygit with a bare repo by passing the --git-dir and --work-tree arguments as you would for the git CLI",
|
||||
|
||||
// general advice
|
||||
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
|
||||
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
|
||||
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
|
||||
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
|
||||
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
|
||||
"The stash is a good place to save snippets of code that you always find yourself adding when debugging.",
|
||||
|
||||
// links
|
||||
fmt.Sprintf(
|
||||
"If you want a git diff with syntax colouring, check out lazygit's integration with delta:\n%s",
|
||||
constants.Links.Docs.CustomPagers,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can build your own custom menus and commands to run from within lazygit. For examples see:\n%s",
|
||||
constants.Links.Docs.CustomCommands,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
|
||||
constants.Links.Issues,
|
||||
),
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randomIndex := rand.Intn(len(tips))
|
||||
return tips[randomIndex]
|
||||
}
|
||||
@@ -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,32 +56,32 @@ 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.WithSpan(gui.Tr.Spans.CheckoutFile).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,
|
||||
prompt: gui.Tr.DiscardFileChangesPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardOldFileChange).DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return false, gui.Errors.ErrSubProcess
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -36,20 +19,17 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
flags = "--no-verify"
|
||||
}
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.clearEditorView(v)
|
||||
_ = gui.returnFromContext()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||
_ = gui.returnFromContext()
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitClose() error {
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
@@ -57,13 +37,13 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
message := utils.ResolvePlaceholderString(
|
||||
gui.Tr.CommitMessageConfirm,
|
||||
map[string]string{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindNewLine": "tab",
|
||||
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
|
||||
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
|
||||
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", message)
|
||||
gui.renderString(gui.Views.Options, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,6 +56,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)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,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 +37,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 +88,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 +119,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 +129,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 +144,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
|
||||
}
|
||||
@@ -170,14 +171,14 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.SureSquashThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashCommitDown).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -199,14 +200,14 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.SureFixupThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.FixupCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -237,7 +238,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.LcRenameCommit,
|
||||
initialContent: message,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.RenameCommit(response); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RenameCommit(response); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -246,7 +247,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
|
||||
}
|
||||
@@ -259,13 +260,12 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
subProcess, err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -288,6 +288,12 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
|
||||
"Update rebase TODO",
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
@@ -295,7 +301,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
|
||||
}
|
||||
@@ -313,24 +319,35 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.DeleteCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.DropCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitDown
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -339,7 +356,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
}
|
||||
@@ -347,7 +364,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
|
||||
}
|
||||
@@ -356,8 +373,19 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitUp
|
||||
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -366,7 +394,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index-1)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx--
|
||||
}
|
||||
@@ -374,7 +402,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
|
||||
}
|
||||
@@ -388,12 +416,12 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.EditCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -403,14 +431,14 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.AmendCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AmendCommit).AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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,19 +453,53 @@ 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
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
|
||||
if commit.IsMerge() {
|
||||
return gui.createRevertMergeCommitMenu(commit)
|
||||
} else {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.afterRevertCommit()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
|
||||
menuItems := make([]*menuItem, len(commit.Parents))
|
||||
for i, parentSha := range commit.Parents {
|
||||
i := i
|
||||
message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
|
||||
onPress: func() error {
|
||||
parentNumber := i + 1
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).RevertMerge(commit.Sha, parentNumber); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.afterRevertCommit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SelectParentCommitForMerge, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterRevertCommit() error {
|
||||
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 +508,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
|
||||
}
|
||||
@@ -470,7 +532,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.CreateFixupCommit,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateFixupCommit).CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -479,7 +541,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
|
||||
}
|
||||
@@ -501,14 +563,14 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashAllAboveFixupCommits).SquashAllAboveFixupCommits(commit.Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -524,15 +586,15 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagNameTitle,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).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
|
||||
@@ -542,12 +604,12 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.LcCheckoutCommit,
|
||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutCommit})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
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 +618,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +659,7 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyCommitMessageToClipboard).CopyToClipboard(message); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,9 @@ package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -108,19 +107,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 +126,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 +169,62 @@ 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)
|
||||
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()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
|
||||
gui.renderString("confirmation", opts.prompt)
|
||||
if opts.editable {
|
||||
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.setKeyBindings(opts)
|
||||
})
|
||||
@@ -248,10 +240,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 +276,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 +296,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,9 +309,14 @@ 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))
|
||||
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -335,11 +332,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,131 +6,25 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type ContextKind int
|
||||
|
||||
const (
|
||||
SIDE_CONTEXT int = iota
|
||||
SIDE_CONTEXT ContextKind = iota
|
||||
MAIN_CONTEXT
|
||||
TEMPORARY_POPUP
|
||||
PERSISTENT_POPUP
|
||||
EXTRAS_CONTEXT
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
var allContextKeys = []string{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -138,262 +32,22 @@ type Context interface {
|
||||
GetOptionsMap() map[string]string
|
||||
}
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
Kind int
|
||||
Key string
|
||||
ViewName string
|
||||
}
|
||||
|
||||
func (c BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
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 nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c BasicContext) SetWindowName(windowName string) {
|
||||
panic("can't set window name on basic context")
|
||||
}
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
func (c BasicContext) GetWindowName() string {
|
||||
// TODO: fix this up
|
||||
return c.GetViewName()
|
||||
}
|
||||
|
||||
func (c BasicContext) SetParentContext(Context) {
|
||||
panic("can't set parent context on basic context")
|
||||
}
|
||||
|
||||
func (c BasicContext) GetParentContext() (Context, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKind() int {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKey() string {
|
||||
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,
|
||||
},
|
||||
},
|
||||
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(),
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
Merging: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.refreshMergePanel,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
},
|
||||
Credentials: SimpleContextNode{
|
||||
Context: BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) 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,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{gui.Contexts.Branches.Context},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Remotes.Context,
|
||||
gui.Contexts.Remotes.Branches.Context,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{gui.Contexts.Tags.Context},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{gui.Contexts.BranchCommits.Context},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
gui.Contexts.ReflogCommits.Context,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{gui.Contexts.Files.Context},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Submodules.Context,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
stack := gui.State.ContextStack
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
for i := range stack {
|
||||
reversedIndex := len(stack) - 1 - i
|
||||
@@ -411,11 +65,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 +83,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.currentContextWithoutLock().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 +128,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 +156,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 +185,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 +205,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 +222,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 +234,7 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
v.Context = c.GetKey()
|
||||
v.Context = string(c.GetKey())
|
||||
|
||||
gui.g.Cursor = v.Editable
|
||||
|
||||
@@ -578,29 +255,50 @@ 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()
|
||||
|
||||
return gui.currentContextWithoutLock()
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextWithoutLock() Context {
|
||||
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 +306,45 @@ 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()
|
||||
}
|
||||
|
||||
// static as opposed to popup
|
||||
func (gui *Gui) currentStaticContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
if len(stack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// find the first context in the stack without a popup type
|
||||
for i := range stack {
|
||||
context := stack[len(stack)-1-i]
|
||||
|
||||
if context.GetKind() != TEMPORARY_POPUP && context.GetKind() != PERSISTENT_POPUP {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
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 +355,7 @@ func (gui *Gui) setInitialViewContexts() {
|
||||
continue
|
||||
}
|
||||
|
||||
view.Context = context.GetKey()
|
||||
view.Context = string(context.GetKey())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -664,18 +385,19 @@ func (gui *Gui) onViewFocusChange() error {
|
||||
|
||||
currentView := gui.g.CurrentView()
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view.Name() != "main" && view == currentView
|
||||
view.Highlight = view.Name() != "main" && view.Name() != "extras" && view == currentView
|
||||
}
|
||||
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 +409,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 +426,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 +442,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 +468,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 +478,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 +488,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 +522,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
|
||||
// }
|
||||
|
||||
266
pkg/gui/context_config.go
Normal file
266
pkg/gui/context_config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package gui
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
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"
|
||||
COMMAND_LOG_CONTEXT_KEY ContextKey = "cmdLog"
|
||||
)
|
||||
|
||||
var allContextKeys = []ContextKey{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
SUGGESTIONS_CONTEXT_KEY,
|
||||
COMMAND_LOG_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Status Context
|
||||
Files *ListContext
|
||||
Submodules *ListContext
|
||||
Menu *ListContext
|
||||
Branches *ListContext
|
||||
Remotes *ListContext
|
||||
RemoteBranches *ListContext
|
||||
Tags *ListContext
|
||||
BranchCommits *ListContext
|
||||
CommitFiles *ListContext
|
||||
ReflogCommits *ListContext
|
||||
SubCommits *ListContext
|
||||
Stash *ListContext
|
||||
Suggestions *ListContext
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
Merging Context
|
||||
Credentials Context
|
||||
Confirmation Context
|
||||
CommitMessage Context
|
||||
Search Context
|
||||
CommandLog Context
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []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,
|
||||
gui.State.Contexts.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
Merging: &BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Credentials: &BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
Confirmation: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
Suggestions: gui.suggestionsListContext(),
|
||||
CommitMessage: &BasicContext{
|
||||
OnFocus: gui.handleCommitMessageFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
Search: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
CommandLog: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: EXTRAS_CONTEXT,
|
||||
ViewName: "extras",
|
||||
Key: COMMAND_LOG_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
OnFocusLost: func() error {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]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,
|
||||
"extras": tree.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{tree.Branches},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
tree.Remotes,
|
||||
tree.RemoteBranches,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{tree.Tags},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{tree.BranchCommits},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
tree.ReflogCommits,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{tree.Files},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
tree.Submodules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,18 +39,19 @@ 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
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -77,6 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
||||
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
|
||||
errMessage = gui.Tr.PassUnameWrong
|
||||
}
|
||||
_ = gui.returnFromContext()
|
||||
// we are not logging this error because it may contain a password or a passphrase
|
||||
_ = gui.createErrorPanel(errMessage)
|
||||
} else {
|
||||
|
||||
@@ -1,50 +1,238 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type commandMenuEntry struct {
|
||||
label string
|
||||
value 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)
|
||||
}
|
||||
|
||||
func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
initialContent: initialValue,
|
||||
handleConfirm: func(str string) error {
|
||||
promptResponses[responseIdx] = str
|
||||
return wrappedF()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
// need to make a menu here some how
|
||||
menuItems := make([]*menuItem, len(prompt.Options))
|
||||
for i, option := range prompt.Options {
|
||||
option := option
|
||||
|
||||
nameTemplate := option.Name
|
||||
if nameTemplate == "" {
|
||||
// this allows you to only pass values rather than bother with names/descriptions
|
||||
nameTemplate = option.Value
|
||||
}
|
||||
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
description, err := gui.resolveTemplate(option.Description, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
value, err := gui.resolveTemplate(option.Value, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{name, style.FgYellow.Sprint(description)},
|
||||
onPress: func() error {
|
||||
promptResponses[responseIdx] = value
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
|
||||
reg, err := regexp.Compile(filter)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
|
||||
}
|
||||
|
||||
buff := bytes.NewBuffer(nil)
|
||||
|
||||
valueTemp, err := template.New("format").Parse(valueFormat)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error()))
|
||||
}
|
||||
|
||||
colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
|
||||
|
||||
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error()))
|
||||
}
|
||||
|
||||
candidates := []commandMenuEntry{}
|
||||
for _, str := range strings.Split(string(commandOutput), "\n") {
|
||||
if str == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tmplData := map[string]string{}
|
||||
out := reg.FindAllStringSubmatch(str, -1)
|
||||
if len(out) > 0 {
|
||||
for groupIdx, group := range reg.SubexpNames() {
|
||||
// Record matched group with group ids
|
||||
matchName := "group_" + strconv.Itoa(groupIdx)
|
||||
tmplData[matchName] = out[0][groupIdx]
|
||||
// Record last named group non-empty matches as group matches
|
||||
if group != "" {
|
||||
tmplData[group] = out[0][groupIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = valueTemp.Execute(buff, tmplData)
|
||||
if err != nil {
|
||||
return candidates, gui.surfaceError(err)
|
||||
}
|
||||
entry := commandMenuEntry{
|
||||
value: strings.TrimSpace(buff.String()),
|
||||
}
|
||||
|
||||
if labelFormat != "" {
|
||||
buff.Reset()
|
||||
err = descTemp.Execute(buff, tmplData)
|
||||
if err != nil {
|
||||
return candidates, gui.surfaceError(err)
|
||||
}
|
||||
entry.label = strings.TrimSpace(buff.String())
|
||||
} else {
|
||||
entry.label = entry.value
|
||||
}
|
||||
|
||||
candidates = append(candidates, entry)
|
||||
|
||||
buff.Reset()
|
||||
}
|
||||
return candidates, err
|
||||
}
|
||||
|
||||
func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
// Collect cmd to run from config
|
||||
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Collect Filter regexp
|
||||
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Run and save output
|
||||
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Need to make a menu out of what the cmd has displayed
|
||||
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems := make([]*menuItem, len(candidates))
|
||||
for i := range candidates {
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{candidates[i].label},
|
||||
onPress: func() error {
|
||||
promptResponses[responseIdx] = candidates[i].value
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
|
||||
return func() error {
|
||||
promptResponses := make([]string, len(customCommand.Prompts))
|
||||
@@ -56,8 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
}
|
||||
|
||||
if customCommand.Subprocess {
|
||||
gui.PrepareSubProcess(cmdStr)
|
||||
return nil
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
}
|
||||
|
||||
loadingText := customCommand.LoadingText
|
||||
@@ -65,9 +252,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.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
@@ -88,72 +273,18 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
switch prompt.Type {
|
||||
case "input":
|
||||
f = func() error {
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
initialContent: initialValue,
|
||||
handleConfirm: func(str string) error {
|
||||
promptResponses[idx] = str
|
||||
|
||||
return wrappedF()
|
||||
},
|
||||
})
|
||||
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
case "menu":
|
||||
f = func() error {
|
||||
// need to make a menu here some how
|
||||
menuItems := make([]*menuItem, len(prompt.Options))
|
||||
for i, option := range prompt.Options {
|
||||
option := option
|
||||
|
||||
nameTemplate := option.Name
|
||||
if nameTemplate == "" {
|
||||
// this allows you to only pass values rather than bother with names/descriptions
|
||||
nameTemplate = option.Value
|
||||
}
|
||||
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
description, err := gui.resolveTemplate(option.Description, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
value, err := gui.resolveTemplate(option.Value, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)},
|
||||
onPress: func() error {
|
||||
promptResponses[idx] = value
|
||||
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
case "menuFromCommand":
|
||||
f = func() error {
|
||||
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
default:
|
||||
return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'")
|
||||
return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -175,9 +306,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 +332,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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
)
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
gui.State.Modes.Diffing = diffing.New()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,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 +51,7 @@ func (gui *Gui) currentDiffTerminals() []string {
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -96,17 +96,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{}
|
||||
@@ -151,7 +147,7 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
|
||||
{
|
||||
displayString: gui.Tr.LcExitDiffMode,
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
gui.State.Modes.Diffing = diffing.New()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.WithSpan(gui.Tr.Spans.DiscardAllChangesInDirectory).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.WithSpan(gui.Tr.Spans.DiscardUnstagedChangesInDirectory).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.WithSpan(gui.Tr.Spans.DiscardAllChangesInFile).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.WithSpan(gui.Tr.Spans.DiscardAllUnstagedChangesInFile).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})
|
||||
}
|
||||
|
||||
21
pkg/gui/dummies.go
Normal file
21
pkg/gui/dummies.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyGui creates a new dummy GUI for testing
|
||||
func NewDummyUpdater() *updates.Updater {
|
||||
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), config.NewDummyAppConfig(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()))
|
||||
return DummyUpdater
|
||||
}
|
||||
|
||||
func NewDummyGui() *Gui {
|
||||
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig(), NewDummyUpdater(), "", false)
|
||||
return DummyGui
|
||||
}
|
||||
@@ -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"
|
||||
49
pkg/gui/extras_panel.go
Normal file
49
pkg/gui/extras_panel.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowCommandLog,
|
||||
onPress: func() error {
|
||||
currentContext := gui.currentStaticContext()
|
||||
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FocusCommandLog,
|
||||
onPress: func() error {
|
||||
return gui.handleFocusCommandLog()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.CommandLog, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFocusCommandLog() error {
|
||||
gui.ShowExtrasWindow = true
|
||||
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext())
|
||||
return gui.pushContext(gui.State.Contexts.CommandLog)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
return gui.scrollUpView(gui.Views.Extras)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
if err := gui.scrollDownView(gui.Views.Extras); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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"
|
||||
@@ -14,66 +8,88 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/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(), gui.State.IgnoreWhitespaceInDiffView)
|
||||
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, gui.State.IgnoreWhitespaceInDiffView)
|
||||
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 +104,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 +114,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 +142,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 +153,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 +172,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 +200,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.WithSpan(gui.Tr.Spans.StageFile).StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageFile).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.WithSpan(gui.Tr.Spans.StageFile).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.WithSpan(gui.Tr.Spans.UnstageFile).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 +254,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,64 +266,88 @@ 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()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageAllFiles).UnstageAll()
|
||||
} else {
|
||||
err = gui.GitCommand.StageAll()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = 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 {
|
||||
gitCommand := gui.GitCommand.WithSpan(gui.Tr.Spans.IgnoreFile)
|
||||
|
||||
unstageFiles := func() error {
|
||||
return node.ForEachFile(func(file *models.File) error {
|
||||
if file.HasStagedChanges {
|
||||
if err := 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 := gitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
|
||||
if err := 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 := 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 {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPreifx == "" {
|
||||
func (gui *Gui) handleWIPCommitPress() error {
|
||||
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPrefix == "" {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
_ = gui.renderStringSync("commitMessage", skipHookPreifx)
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -304,7 +366,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
func (gui *Gui) prepareFilesForCommit() error {
|
||||
noStagedFiles := len(gui.stagedFiles()) == 0
|
||||
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
|
||||
err := gui.GitCommand.StageAll()
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -320,11 +382,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 +399,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
|
||||
}
|
||||
|
||||
@@ -356,7 +421,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
title: gui.Tr.NoFilesStagedTitle,
|
||||
prompt: gui.Tr.NoFilesStagedPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StageAll(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshFilesAndSubmodules(); err != nil {
|
||||
@@ -369,6 +434,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)
|
||||
}
|
||||
@@ -381,17 +450,9 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
title: strings.Title(gui.Tr.AmendLastCommit),
|
||||
prompt: gui.Tr.SureToAmend,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
cmdStr := gui.GitCommand.AmendHeadCmdStr()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -399,84 +460,156 @@ 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.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
_, err := gui.runSyncOrAsyncCommand(gui.GitCommand.EditFile(filename))
|
||||
return err
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.Pull
|
||||
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
@@ -484,7 +617,7 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
// if we have no upstream branch we need to set that first
|
||||
if currentBranch.Pullables == "?" {
|
||||
if !currentBranch.IsTrackingRemote() {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
@@ -492,7 +625,7 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name, span: span})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,17 +640,18 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
@@ -525,9 +659,11 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mode := gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
mode := &gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
*mode = gui.GitCommand.GetPullMode(*mode)
|
||||
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
|
||||
// TODO: this doesn't look like a good idea. Why the goroutine?
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(*mode, opts) })
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -536,7 +672,9 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
err := gui.GitCommand.Fetch(
|
||||
gitCommand := gui.GitCommand.WithSpan(opts.span)
|
||||
|
||||
err := gitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
@@ -550,26 +688,26 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
|
||||
err := gitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
@@ -580,7 +718,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,41 +729,50 @@ 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
|
||||
}
|
||||
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if currentBranch.IsTrackingRemote() {
|
||||
if currentBranch.HasCommitsToPull() {
|
||||
return gui.requestToForcePush()
|
||||
} else {
|
||||
return gui.pushWithForceFlag(false, "", "")
|
||||
}
|
||||
} else {
|
||||
// see if we have an upstream for this branch in our config
|
||||
upstream, err := gui.upstreamForBranchInConfig(currentBranch.Name)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
|
||||
if upstream != "" {
|
||||
return gui.pushWithForceFlag(false, "", upstream)
|
||||
}
|
||||
|
||||
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, "")
|
||||
handleConfirm: func(upstream string) error {
|
||||
return gui.pushWithForceFlag(false, upstream, "")
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(v, false, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) requestToForcePush() error {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
|
||||
@@ -635,11 +782,26 @@ 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, "", "")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) upstreamForBranchInConfig(branchName string) (string, error) {
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for configBranchName, configBranch := range conf.Branches {
|
||||
if configBranchName == branchName {
|
||||
return fmt.Sprintf("%s %s", configBranch.Remote, configBranchName), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
@@ -650,18 +812,18 @@ 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 {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.OpenFile).OpenFile(filename); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
@@ -669,28 +831,30 @@ 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
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
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,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashAllChanges).StashSave)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.LcStashStagedChanges,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashStagedChanges).StashSaveStagedChanges)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -698,10 +862,64 @@ 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
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenMergeTool() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.MergeToolTitle,
|
||||
prompt: gui.Tr.MergeToolPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
140
pkg/gui/filetree/build_tree.go
Normal file
140
pkg/gui/filetree/build_tree.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
root := &FileNode{}
|
||||
|
||||
var curr *FileNode
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
var setFile *models.File
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := join(splitPath[: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 {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
var setFile *models.CommitFile
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := join(splitPath[: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()
|
||||
|
||||
// from top down we have merge conflict files, then tracked file, then untracked
|
||||
// files. This is the one way in which sorting differs between flat mode and
|
||||
// tree mode
|
||||
sort.SliceStable(sortedFiles, func(i, j int) bool {
|
||||
iFile := sortedFiles[i].File
|
||||
jFile := sortedFiles[j].File
|
||||
|
||||
// never going to happen but just to be safe
|
||||
if iFile == nil || jFile == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.HasMergeConflicts && !jFile.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.HasMergeConflicts && !iFile.HasMergeConflicts {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.Tracked && !jFile.Tracked {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.Tracked && !iFile.Tracked {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
return strings.Split(str, "/")
|
||||
}
|
||||
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
530
pkg/gui/filetree/build_tree_test.go
Normal file
530
pkg/gui/filetree/build_tree_test.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted including a merge conflict file",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "z",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "z", HasMergeConflicts: true},
|
||||
Path: "z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tracked, untracked, and conflicted files",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "a2",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "a1",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "c2",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "c1",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "b2",
|
||||
Tracked: true,
|
||||
},
|
||||
{
|
||||
Name: "b1",
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "c2", HasMergeConflicts: true},
|
||||
Path: "c2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b1", Tracked: true},
|
||||
Path: "b1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b2", Tracked: true},
|
||||
Path: "b2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a1", Tracked: false},
|
||||
Path: "a1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a2", Tracked: false},
|
||||
Path: "a2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
Path: "dir1/dir3",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
Path: "dir2/dir4",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
20
pkg/gui/filetree/collapsed_paths.go
Normal file
20
pkg/gui/filetree/collapsed_paths.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package filetree
|
||||
|
||||
type CollapsedPaths map[string]bool
|
||||
|
||||
func (cp CollapsedPaths) ExpandToPath(path string) {
|
||||
// need every directory along the way
|
||||
splitPath := split(path)
|
||||
for i := range splitPath {
|
||||
dir := join(splitPath[0 : i+1])
|
||||
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)
|
||||
})
|
||||
}
|
||||
172
pkg/gui/filetree/commit_file_node.go
Normal file
172
pkg/gui/filetree/commit_file_node.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"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 := split(s.Path)
|
||||
name := 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)
|
||||
})
|
||||
}
|
||||
95
pkg/gui/filetree/file_manager_test.go
Normal file
95
pkg/gui/filetree/file_manager_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
187
pkg/gui/filetree/file_node.go
Normal file
187
pkg/gui/filetree/file_node.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"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 := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := split(s.File.PreviousName)
|
||||
|
||||
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) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
134
pkg/gui/filetree/file_node_test.go
Normal file
134
pkg/gui/filetree/file_node_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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",
|
||||
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/dir3-1",
|
||||
CompressionLevel: 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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].IsLeaf() {
|
||||
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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user