mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-05 21:46:49 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d40d03bb2 | ||
|
|
9a9e3d506d | ||
|
|
06ca71e955 | ||
|
|
308a3b51b3 | ||
|
|
ccd80a0e4b | ||
|
|
37be9dbea1 | ||
|
|
f6ec7babf5 | ||
|
|
802cfb1a04 | ||
|
|
2fc1498517 | ||
|
|
7a464ae5b7 | ||
|
|
927ee63106 | ||
|
|
9989c96321 | ||
|
|
f91892b8f1 | ||
|
|
72bce201df | ||
|
|
5df0fe0765 | ||
|
|
c47c539e12 | ||
|
|
c96496c3a7 | ||
|
|
7561703e8d | ||
|
|
b04b457246 | ||
|
|
6457800748 | ||
|
|
7d9461877a | ||
|
|
e122f421e6 | ||
|
|
6171690b00 | ||
|
|
253504a094 | ||
|
|
f704707d29 | ||
|
|
01d82749b1 | ||
|
|
3eb124c732 | ||
|
|
ef544e6ce9 | ||
|
|
629494144f | ||
|
|
b6a5e9d615 | ||
|
|
5011cac7ea | ||
|
|
5df0475612 | ||
|
|
f6e316dfe5 | ||
|
|
91e8765d9c | ||
|
|
80a8e9b04d | ||
|
|
2008c39516 | ||
|
|
6388af70ac | ||
|
|
5ee559b896 | ||
|
|
ca7252ef8e | ||
|
|
a496858c62 | ||
|
|
40bc3aa5a9 | ||
|
|
e4888e924e | ||
|
|
71fdc5c038 | ||
|
|
a05f22efa2 | ||
|
|
c0cd9dd835 | ||
|
|
305f211615 | ||
|
|
d672b7342f | ||
|
|
e7c27b6f4a | ||
|
|
345c90ac05 | ||
|
|
7564e506b5 | ||
|
|
1e50764b4d | ||
|
|
9619d3447f | ||
|
|
4171b7613c | ||
|
|
92f03a7872 | ||
|
|
2dc8396deb | ||
|
|
7b615e3186 | ||
|
|
a2108362de | ||
|
|
87e9d9bdc2 | ||
|
|
b6454755ca | ||
|
|
3621084096 | ||
|
|
8c25aaa687 | ||
|
|
d02e52989e | ||
|
|
913a2fd065 | ||
|
|
db736896bc | ||
|
|
154b6b09cb | ||
|
|
292b780bd8 | ||
|
|
c421f396af | ||
|
|
a1ae2aa277 | ||
|
|
e19b4fe369 | ||
|
|
eb7531b206 | ||
|
|
428ce2d0f2 | ||
|
|
f1fbf1e9f5 | ||
|
|
268d4080b3 | ||
|
|
2c72990838 | ||
|
|
046edd8120 | ||
|
|
c4552aad28 | ||
|
|
5c57c973d6 | ||
|
|
c5f7ad5adb | ||
|
|
663c036ca5 | ||
|
|
c8e9d1b4fc | ||
|
|
ab0117c416 | ||
|
|
652c97d239 | ||
|
|
bd67bba751 | ||
|
|
5193353020 | ||
|
|
a5719c530a | ||
|
|
add3e8783e | ||
|
|
5eff56b557 | ||
|
|
60c87b3e70 | ||
|
|
66d0fd2133 | ||
|
|
f44ae68e99 | ||
|
|
57f7051590 | ||
|
|
0543d43f10 | ||
|
|
ab8f2b7cc4 | ||
|
|
16dbb6f76e | ||
|
|
51383f24bf | ||
|
|
151486dcfb | ||
|
|
c1d2aa61f3 | ||
|
|
63072af5bc | ||
|
|
44d08edfb0 | ||
|
|
f08fdb2873 | ||
|
|
df4eb70ba2 | ||
|
|
6ca42ff720 | ||
|
|
a533f8e1a5 | ||
|
|
cf8ded0b79 | ||
|
|
73548fa15f | ||
|
|
a0e7604f61 | ||
|
|
aedeba4fe3 | ||
|
|
7033a4bd58 | ||
|
|
c3d7de1c18 | ||
|
|
711bd5a670 | ||
|
|
6b68f4f25d | ||
|
|
89ee0a1dee | ||
|
|
2dc6f5f079 | ||
|
|
bdea3b7dcf | ||
|
|
51f05ce08b | ||
|
|
487ad196a7 | ||
|
|
44140adb92 | ||
|
|
508af269fb | ||
|
|
0af0e66586 | ||
|
|
821a59f21d | ||
|
|
5ea3dc7579 | ||
|
|
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 | ||
|
|
ac609bd37c | ||
|
|
67cc65930a | ||
|
|
4f66093335 | ||
|
|
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 |
@@ -0,0 +1,9 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
allowed_updates:
|
||||||
|
- match:
|
||||||
|
update_type: "security"
|
||||||
@@ -29,26 +29,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||||
formula: lazygit
|
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
|
|
||||||
|
|||||||
+37
-12
@@ -8,7 +8,14 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
|
name: ci - ${{matrix.os}}
|
||||||
|
runs-on: ${{matrix.os}}
|
||||||
env:
|
env:
|
||||||
GOFLAGS: -mod=vendor
|
GOFLAGS: -mod=vendor
|
||||||
steps:
|
steps:
|
||||||
@@ -22,19 +29,37 @@ jobs:
|
|||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/go-build
|
path: ~/.cache/go-build
|
||||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
|
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{runner.os}}-go-
|
${{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
|
- name: Test code
|
||||||
run: |
|
run: |
|
||||||
./test.sh
|
bash ./test.sh
|
||||||
- name: Build binaries
|
build:
|
||||||
uses: goreleaser/goreleaser-action@v1
|
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:
|
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
|
||||||
|
|||||||
@@ -12,3 +12,9 @@ jobs:
|
|||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
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
|
||||||
|
|||||||
+3
-1
@@ -27,8 +27,10 @@ lazygit
|
|||||||
|
|
||||||
test/git_server/data
|
test/git_server/data
|
||||||
test/integration/*/actual/
|
test/integration/*/actual/
|
||||||
|
test/integration/*/actual_remote/
|
||||||
test/integration/*/used_config/
|
test/integration/*/used_config/
|
||||||
# these sample hooks waste too space space
|
# these sample hooks waste too much space
|
||||||
test/integration/*/expected/.git_keep/hooks/
|
test/integration/*/expected/.git_keep/hooks/
|
||||||
|
test/integration/*/expected_remote/hooks/
|
||||||
!.git_keep/
|
!.git_keep/
|
||||||
lazygit.exe
|
lazygit.exe
|
||||||
|
|||||||
+20
-3
@@ -1,14 +1,13 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
|
|
||||||
♥ We love pull requests from everyone !
|
♥ We love pull requests from everyone !
|
||||||
|
|
||||||
|
|
||||||
When contributing to this repository, please first discuss the change you wish
|
When contributing to this repository, please first discuss the change you wish
|
||||||
to make via issue, email, or any other method with the owners of this repository
|
to make via issue, email, or any other method with the owners of this repository
|
||||||
before making a change.
|
before making a change.
|
||||||
|
|
||||||
## So all code changes happen through Pull Requests
|
## So all code changes happen through Pull Requests
|
||||||
|
|
||||||
Pull requests are the best way to propose changes to the codebase. We actively
|
Pull requests are the best way to propose changes to the codebase. We actively
|
||||||
welcome your pull requests:
|
welcome your pull requests:
|
||||||
|
|
||||||
@@ -21,15 +20,33 @@ welcome your pull requests:
|
|||||||
7. Issue that pull request!
|
7. Issue that pull request!
|
||||||
|
|
||||||
## Code of conduct
|
## Code of conduct
|
||||||
|
|
||||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||||
|
|
||||||
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
||||||
|
|
||||||
## Any contributions you make will be under the MIT Software License
|
## Any contributions you make will be under the MIT Software License
|
||||||
|
|
||||||
In short, when you submit code changes, your submissions are understood to be
|
In short, when you submit code changes, your submissions are understood to be
|
||||||
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
||||||
covers the project. Feel free to contact the maintainers if that's a concern.
|
covers the project. Feel free to contact the maintainers if that's a concern.
|
||||||
|
|
||||||
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
||||||
|
|
||||||
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
||||||
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
||||||
|
|
||||||
|
## Updating Gocui
|
||||||
|
|
||||||
|
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rending windows and handling user input. Here's the typical process to follow:
|
||||||
|
|
||||||
|
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
|
||||||
|
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
|
||||||
|
3. Raise a PR on the gocui repo with your changes
|
||||||
|
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./bump_gocui.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Raise a PR in lazygit with those changes
|
||||||
|
|||||||
@@ -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)
|
- [Binary releases](#binary-releases)
|
||||||
- [Homebrew](#homebrew)
|
- [Homebrew](#homebrew)
|
||||||
- [MacPorts](#macports)
|
- [MacPorts](#macports)
|
||||||
- [Ubuntu](#ubuntu)
|
|
||||||
- [Void Linux](#void-linux)
|
- [Void Linux](#void-linux)
|
||||||
- [Scoop (Windows)](#scoop-windows)
|
- [Scoop (Windows)](#scoop-windows)
|
||||||
- [Arch Linux](#arch-linux)
|
- [Arch Linux](#arch-linux)
|
||||||
@@ -53,7 +52,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
|
|||||||
|
|
||||||
### Binary Releases
|
### Binary Releases
|
||||||
|
|
||||||
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
|
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
|
||||||
|
|
||||||
### Homebrew
|
### Homebrew
|
||||||
|
|
||||||
@@ -82,6 +81,8 @@ sudo port install lazygit
|
|||||||
|
|
||||||
### Ubuntu
|
### Ubuntu
|
||||||
|
|
||||||
|
**Deprecated**: will no longer receive updates.
|
||||||
|
|
||||||
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -114,12 +115,12 @@ scoop install lazygit
|
|||||||
|
|
||||||
### Arch Linux
|
### 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
|
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.
|
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/>
|
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
|
||||||
|
|
||||||
Instruction of how to install AUR content can be found here:
|
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)
|
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
|
### I'm struggling to see the selected line
|
||||||
is my first project in Go the code could no doubt use an increase in quality,
|
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
|
||||||
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).
|
|
||||||
|
|
||||||
## Social
|
## Social
|
||||||
|
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
|
||||||
|
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
|
||||||
|
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
|
||||||
|
|
||||||
|
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
|
||||||
+348
-232
@@ -2,181 +2,219 @@
|
|||||||
|
|
||||||
Default path for the config file:
|
Default path for the config file:
|
||||||
|
|
||||||
* Linux: `~/.config/lazygit/config.yml`
|
- Linux: `~/.config/lazygit/config.yml`
|
||||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
- MacOS: `~/Library/Application Support/lazygit/config.yml`
|
||||||
* Windows: `%APPDATA%\jesseduffield\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
|
## Default
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
gui:
|
gui:
|
||||||
# stuff relating to the UI
|
# stuff relating to the UI
|
||||||
scrollHeight: 2 # how many lines you scroll by
|
scrollHeight: 2 # how many lines you scroll by
|
||||||
scrollPastBottom: true # enable scrolling past the bottom
|
scrollPastBottom: true # enable scrolling past the bottom
|
||||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||||
expandFocusedSidePanel: false
|
expandFocusedSidePanel: false
|
||||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||||
theme:
|
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl'
|
||||||
lightTheme: false # For terminals with a light background
|
theme:
|
||||||
activeBorderColor:
|
lightTheme: false # For terminals with a light background
|
||||||
- white
|
activeBorderColor:
|
||||||
- bold
|
- white
|
||||||
inactiveBorderColor:
|
- bold
|
||||||
- green
|
inactiveBorderColor:
|
||||||
optionsTextColor:
|
- green
|
||||||
- blue
|
optionsTextColor:
|
||||||
selectedLineBgColor:
|
- blue
|
||||||
- default
|
selectedLineBgColor:
|
||||||
selectedRangeBgColor:
|
- default
|
||||||
- blue
|
selectedRangeBgColor:
|
||||||
commitLength:
|
- blue
|
||||||
show: true
|
cherryPickedCommitBgColor:
|
||||||
mouseEvents: true
|
- blue
|
||||||
skipUnstageLineWarning: false
|
cherryPickedCommitFgColor:
|
||||||
skipStashWarning: true
|
- cyan
|
||||||
git:
|
commitLength:
|
||||||
paging:
|
show: true
|
||||||
colorArg: always
|
mouseEvents: true
|
||||||
useConfig: false
|
skipUnstageLineWarning: false
|
||||||
merging:
|
skipStashWarning: true
|
||||||
# only applicable to unix users
|
showFileTree: false # for rendering changes files in a tree format
|
||||||
manualCommit: false
|
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||||
# extra args passed to `git merge`, e.g. --no-ff
|
showRandomTip: true
|
||||||
args: ""
|
showCommandLog: true
|
||||||
pull:
|
commandLogSize: 8
|
||||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
authorColors: # in case you're not happy with the randomly assigned colour
|
||||||
skipHookPrefix: WIP
|
'John Smith': '#ff0000'
|
||||||
autoFetch: true
|
git:
|
||||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
paging:
|
||||||
allBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
|
colorArg: always
|
||||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
useConfig: false
|
||||||
disableForcePushing: false
|
merging:
|
||||||
refresher:
|
# only applicable to unix users
|
||||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
manualCommit: false
|
||||||
fetchInterval: 60 # re-fetch interval in seconds
|
# extra args passed to `git merge`, e.g. --no-ff
|
||||||
update:
|
args: ''
|
||||||
method: prompt # can be: prompt | background | never
|
log:
|
||||||
days: 14 # how often an update is checked for
|
# one of date-order, author-date-order, topo-order.
|
||||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
# topo-order makes it easier to read the git log graph, but commits may not
|
||||||
confirmOnQuit: false
|
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
|
||||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
order: 'topo-order'
|
||||||
quitOnTopLevelReturn: true
|
# one of always, never, when-maximised
|
||||||
disableStartupPopups: false
|
# this determines whether the git graph is rendered in the commits panel
|
||||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
showGraph: 'when-maximised'
|
||||||
keybinding:
|
skipHookPrefix: WIP
|
||||||
universal:
|
autoFetch: true
|
||||||
quit: 'q'
|
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||||
quitWithoutChangingDirectory: 'Q'
|
disableForcePushing: false
|
||||||
togglePanel: '<tab>' # goto the next panel
|
parseEmoji: false
|
||||||
prevItem: '<up>' # go one line up
|
os:
|
||||||
nextItem: '<down>' # go one line down
|
editCommand: '' # see 'Configuring File Editing' section
|
||||||
prevItem-alt: 'k' # go one line up
|
editCommandTemplate: '{{editor}} {{filename}}'
|
||||||
nextItem-alt: 'j' # go one line down
|
openCommand: ''
|
||||||
prevPage: ',' # go to next page in list
|
refresher:
|
||||||
nextPage: '.' # go to previous page in list
|
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||||
gotoTop: '<' # go to top of list
|
fetchInterval: 60 # re-fetch interval in seconds
|
||||||
gotoBottom: '>' # go to bottom of list
|
update:
|
||||||
prevBlock: '<left>' # goto the previous block / panel
|
method: prompt # can be: prompt | background | never
|
||||||
nextBlock: '<right>' # goto the next block / panel
|
days: 14 # how often an update is checked for
|
||||||
prevBlock-alt: 'h' # goto the previous block / panel
|
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||||
nextBlock-alt: 'l' # goto the next block / panel
|
confirmOnQuit: false
|
||||||
nextMatch: 'n'
|
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||||
prevMatch: 'N'
|
quitOnTopLevelReturn: false
|
||||||
optionMenu: 'x' # show help menu
|
disableStartupPopups: false
|
||||||
optionMenu-alt1: '?' # show help menu
|
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||||
select: '<space>'
|
keybinding:
|
||||||
goInto: '<enter>'
|
universal:
|
||||||
confirm: '<enter>'
|
quit: 'q'
|
||||||
confirm-alt1: 'y'
|
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||||
remove: 'd'
|
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||||
new: 'n'
|
quitWithoutChangingDirectory: 'Q'
|
||||||
edit: 'e'
|
togglePanel: '<tab>' # goto the next panel
|
||||||
openFile: 'o'
|
prevItem: '<up>' # go one line up
|
||||||
scrollUpMain: '<pgup>' # main panel scrool up
|
nextItem: '<down>' # go one line down
|
||||||
scrollDownMain: '<pgdown>' # main panel scrool down
|
prevItem-alt: 'k' # go one line up
|
||||||
scrollUpMain-alt1: 'K' # main panel scrool up
|
nextItem-alt: 'j' # go one line down
|
||||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
prevPage: ',' # go to next page in list
|
||||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
nextPage: '.' # go to previous page in list
|
||||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
gotoTop: '<' # go to top of list
|
||||||
executeCustomCommand: ':'
|
gotoBottom: '>' # go to bottom of list
|
||||||
createRebaseOptionsMenu: 'm'
|
scrollLeft: 'H' # scroll left within list view
|
||||||
pushFiles: 'P'
|
scrollRight: 'L' # scroll right within list view
|
||||||
pullFiles: 'p'
|
prevBlock: '<left>' # goto the previous block / panel
|
||||||
refresh: 'R'
|
nextBlock: '<right>' # goto the next block / panel
|
||||||
createPatchOptionsMenu: '<c-p>'
|
prevBlock-alt: 'h' # goto the previous block / panel
|
||||||
nextTab: ']'
|
nextBlock-alt: 'l' # goto the next block / panel
|
||||||
prevTab: '['
|
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||||
nextScreenMode: '+'
|
nextMatch: 'n'
|
||||||
prevScreenMode: '_'
|
prevMatch: 'N'
|
||||||
undo: 'z'
|
optionMenu: 'x' # show help menu
|
||||||
redo: '<c-z>'
|
optionMenu-alt1: '?' # show help menu
|
||||||
filteringMenu: '<c-s>'
|
select: '<space>'
|
||||||
diffingMenu: 'W'
|
goInto: '<enter>'
|
||||||
diffingMenu-alt: '<c-e>' # deprecated
|
openRecentRepos: '<c-r>'
|
||||||
copyToClipboard: '<c-o>'
|
confirm: '<enter>'
|
||||||
submitEditorText: '<enter>'
|
confirm-alt1: 'y'
|
||||||
appendNewline: '<tab>'
|
remove: 'd'
|
||||||
status:
|
new: 'n'
|
||||||
checkForUpdate: 'u'
|
edit: 'e'
|
||||||
recentRepos: '<enter>'
|
openFile: 'o'
|
||||||
files:
|
scrollUpMain: '<pgup>' # main panel scroll up
|
||||||
commitChanges: 'c'
|
scrollDownMain: '<pgdown>' # main panel scroll down
|
||||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
scrollUpMain-alt1: 'K' # main panel scroll up
|
||||||
amendLastCommit: 'A'
|
scrollDownMain-alt1: 'J' # main panel scroll down
|
||||||
commitChangesWithEditor: 'C'
|
scrollUpMain-alt2: '<c-u>' # main panel scroll up
|
||||||
ignoreFile: 'i'
|
scrollDownMain-alt2: '<c-d>' # main panel scroll down
|
||||||
refreshFiles: 'r'
|
executeCustomCommand: ':'
|
||||||
stashAllChanges: 's'
|
createRebaseOptionsMenu: 'm'
|
||||||
viewStashOptions: 'S'
|
pushFiles: 'P'
|
||||||
toggleStagedAll: 'a' # stage/unstage all
|
pullFiles: 'p'
|
||||||
viewResetOptions: 'D'
|
refresh: 'R'
|
||||||
fetch: 'f'
|
createPatchOptionsMenu: '<c-p>'
|
||||||
branches:
|
nextTab: ']'
|
||||||
createPullRequest: 'o'
|
prevTab: '['
|
||||||
checkoutBranchByName: 'c'
|
nextScreenMode: '+'
|
||||||
forceCheckoutBranch: 'F'
|
prevScreenMode: '_'
|
||||||
rebaseBranch: 'r'
|
undo: 'z'
|
||||||
mergeIntoCurrentBranch: 'M'
|
redo: '<c-z>'
|
||||||
viewGitFlowOptions: 'i'
|
filteringMenu: '<c-s>'
|
||||||
fastForward: 'f' # fast-forward this branch from its upstream
|
diffingMenu: 'W'
|
||||||
pushTag: 'P'
|
diffingMenu-alt: '<c-e>' # deprecated
|
||||||
setUpstream: 'u' # set as upstream of checked-out branch
|
copyToClipboard: '<c-o>'
|
||||||
fetchRemote: 'f'
|
submitEditorText: '<enter>'
|
||||||
commits:
|
appendNewline: '<a-enter>'
|
||||||
squashDown: 's'
|
extrasMenu: '@'
|
||||||
renameCommit: 'r'
|
toggleWhitespaceInDiffView: '<c-w>'
|
||||||
renameCommitWithEditor: 'R'
|
status:
|
||||||
viewResetOptions: 'g'
|
checkForUpdate: 'u'
|
||||||
markCommitAsFixup: 'f'
|
recentRepos: '<enter>'
|
||||||
createFixupCommit: 'F' # create fixup commit for this commit
|
files:
|
||||||
squashAboveCommits: 'S'
|
commitChanges: 'c'
|
||||||
moveDownCommit: '<c-j>' # move commit down one
|
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||||
moveUpCommit: '<c-k>' # move commit up one
|
amendLastCommit: 'A'
|
||||||
amendToCommit: 'A'
|
commitChangesWithEditor: 'C'
|
||||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
ignoreFile: 'i'
|
||||||
revertCommit: 't'
|
refreshFiles: 'r'
|
||||||
cherryPickCopy: 'c'
|
stashAllChanges: 's'
|
||||||
cherryPickCopyRange: 'C'
|
viewStashOptions: 'S'
|
||||||
pasteCommits: 'v'
|
toggleStagedAll: 'a' # stage/unstage all
|
||||||
tagCommit: 'T'
|
viewResetOptions: 'D'
|
||||||
checkoutCommit: '<space>'
|
fetch: 'f'
|
||||||
resetCherryPick: '<c-R>'
|
toggleTreeView: '`'
|
||||||
copyCommitMessageToClipboard: '<c-y>'
|
branches:
|
||||||
stash:
|
createPullRequest: 'o'
|
||||||
popStash: 'g'
|
viewPullRequestOptions: 'O'
|
||||||
commitFiles:
|
checkoutBranchByName: 'c'
|
||||||
checkoutCommitFile: 'c'
|
forceCheckoutBranch: 'F'
|
||||||
main:
|
rebaseBranch: 'r'
|
||||||
toggleDragSelect: 'v'
|
mergeIntoCurrentBranch: 'M'
|
||||||
toggleDragSelect-alt: 'V'
|
viewGitFlowOptions: 'i'
|
||||||
toggleSelectHunk: 'a'
|
fastForward: 'f' # fast-forward this branch from its upstream
|
||||||
pickBothHunks: 'b'
|
pushTag: 'P'
|
||||||
submodules:
|
setUpstream: 'u' # set as upstream of checked-out branch
|
||||||
init: 'i'
|
fetchRemote: 'f'
|
||||||
update: 'u'
|
commits:
|
||||||
bulkMenu: 'b'
|
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>'
|
||||||
|
openLogMenu: '<c-l>'
|
||||||
|
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
|
## Platform Defaults
|
||||||
@@ -184,22 +222,73 @@ Default path for the config file:
|
|||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
os:
|
os:
|
||||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
openCommand: 'start "" {{filename}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
os:
|
os:
|
||||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
openCommand: 'xdg-open {{filename}} >/dev/null'
|
||||||
```
|
```
|
||||||
|
|
||||||
### OSX
|
### OSX
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
os:
|
os:
|
||||||
openCommand: 'open {{filename}}'
|
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.
|
||||||
|
|
||||||
|
You can specify a line number you are currently at when in the line-by-line mode.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
os:
|
||||||
|
editCommand: 'vim'
|
||||||
|
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
os:
|
||||||
|
editCommand: 'code'
|
||||||
|
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||||
|
|
||||||
|
### Overriding default config file location
|
||||||
|
|
||||||
|
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
|
||||||
|
|
||||||
|
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
|
||||||
|
|
||||||
|
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
|
||||||
|
or
|
||||||
|
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recommended Config Values
|
### Recommended Config Values
|
||||||
@@ -207,8 +296,8 @@ Default path for the config file:
|
|||||||
for users of VSCode
|
for users of VSCode
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
os:
|
os:
|
||||||
openCommand: 'code -rg {{filename}}'
|
openCommand: 'code -rg {{filename}}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Color Attributes
|
## Color Attributes
|
||||||
@@ -216,7 +305,8 @@ for users of VSCode
|
|||||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||||
The available attributes are:
|
The available attributes are:
|
||||||
|
|
||||||
- default
|
**Colors**
|
||||||
|
|
||||||
- black
|
- black
|
||||||
- red
|
- red
|
||||||
- green
|
- green
|
||||||
@@ -225,7 +315,12 @@ The available attributes are:
|
|||||||
- magenta
|
- magenta
|
||||||
- cyan
|
- cyan
|
||||||
- white
|
- white
|
||||||
|
- '#ff00ff'
|
||||||
|
|
||||||
|
**Modifiers**
|
||||||
|
|
||||||
- bold
|
- bold
|
||||||
|
- default
|
||||||
- reverse # useful for high-contrast
|
- reverse # useful for high-contrast
|
||||||
- underline
|
- underline
|
||||||
|
|
||||||
@@ -234,31 +329,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
|
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
gui:
|
gui:
|
||||||
theme:
|
theme:
|
||||||
lightTheme: true
|
lightTheme: true
|
||||||
activeBorderColor:
|
activeBorderColor:
|
||||||
- black
|
- black
|
||||||
- bold
|
- bold
|
||||||
inactiveBorderColor:
|
inactiveBorderColor:
|
||||||
- black
|
- black
|
||||||
selectedLineBgColor:
|
selectedLineBgColor:
|
||||||
- default
|
- default
|
||||||
```
|
```
|
||||||
|
|
||||||
## Struggling to see selected line
|
## 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
|
```yaml
|
||||||
gui:
|
gui:
|
||||||
theme:
|
theme:
|
||||||
selectedLineBgColor:
|
selectedLineBgColor:
|
||||||
- reverse
|
- reverse
|
||||||
selectedRangeBgColor:
|
selectedRangeBgColor:
|
||||||
- reverse
|
- 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
|
## Example Coloring
|
||||||
|
|
||||||

|

|
||||||
@@ -270,33 +384,33 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
|
|||||||
### Example Keybindings For Colemak Users
|
### Example Keybindings For Colemak Users
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
keybinding:
|
keybinding:
|
||||||
universal:
|
universal:
|
||||||
prevItem-alt: 'u'
|
prevItem-alt: 'u'
|
||||||
nextItem-alt: 'e'
|
nextItem-alt: 'e'
|
||||||
prevBlock-alt: 'n'
|
prevBlock-alt: 'n'
|
||||||
nextBlock-alt: 'i'
|
nextBlock-alt: 'i'
|
||||||
nextMatch: '='
|
nextMatch: '='
|
||||||
prevMatch: '-'
|
prevMatch: '-'
|
||||||
new: 'k'
|
new: 'k'
|
||||||
edit: 'o'
|
edit: 'o'
|
||||||
openFile: 'O'
|
openFile: 'O'
|
||||||
scrollUpMain-alt1: 'U'
|
scrollUpMain-alt1: 'U'
|
||||||
scrollDownMain-alt1: 'E'
|
scrollDownMain-alt1: 'E'
|
||||||
scrollUpMain-alt2: '<c-u>'
|
scrollUpMain-alt2: '<c-u>'
|
||||||
scrollDownMain-alt2: '<c-e>'
|
scrollDownMain-alt2: '<c-e>'
|
||||||
undo: 'l'
|
undo: 'l'
|
||||||
redo: '<c-r>'
|
redo: '<c-r>'
|
||||||
diffingMenu: 'M'
|
diffingMenu: 'M'
|
||||||
filteringMenu: '<c-f>'
|
filteringMenu: '<c-f>'
|
||||||
files:
|
files:
|
||||||
ignoreFile: 'I'
|
ignoreFile: 'I'
|
||||||
commits:
|
commits:
|
||||||
moveDownCommit: '<c-e>'
|
moveDownCommit: '<c-e>'
|
||||||
moveUpCommit: '<c-u>'
|
moveUpCommit: '<c-u>'
|
||||||
branches:
|
branches:
|
||||||
viewGitFlowOptions: 'I'
|
viewGitFlowOptions: 'I'
|
||||||
setUpstream: 'U'
|
setUpstream: 'U'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom pull request URLs
|
## Custom pull request URLs
|
||||||
@@ -307,7 +421,7 @@ the pull request. You can do so on your `config.yml` file using the following sy
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
"<gitDomain>": "<provider>:<webDomain>"
|
'<gitDomain>': '<provider>:<webDomain>'
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
@@ -317,19 +431,21 @@ Where:
|
|||||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||||
|
|
||||||
## Predefined commit message prefix
|
## Predefined commit message prefix
|
||||||
|
|
||||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
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.
|
commit message with prefix that is parsed from the branch name.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
* Branch name: feature/AB-123
|
|
||||||
* Commit message: [AB-123] Adding feature
|
- Branch name: feature/AB-123
|
||||||
|
- Commit message: [AB-123] Adding feature
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
git:
|
git:
|
||||||
commitPrefixes:
|
commitPrefixes:
|
||||||
my_project: # This is repository folder name
|
my_project: # This is repository folder name
|
||||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||||
replace: "[$1] "
|
replace: '[$1] '
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom git log command
|
## Custom git log command
|
||||||
|
|||||||
@@ -35,6 +35,20 @@ customCommands:
|
|||||||
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
|
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
|
||||||
context: 'localBranches'
|
context: 'localBranches'
|
||||||
loadingText: 'creating branch'
|
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:
|
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:
|
The permitted prompt fields are:
|
||||||
|
|
||||||
| _field_ | _description_ | _required_ |
|
| _field_ | _description_ | _required_ |
|
||||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||||
| type | one of 'input' or 'menu' | yes |
|
| type | one of 'input' or 'menu' | yes |
|
||||||
| title | the title to display in the popup panel | no |
|
| 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 |
|
| 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 |
|
| 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:
|
The permitted option fields are:
|
||||||
| _field_ | _description_ | _required_ |
|
| _field_ | _description_ | _required_ |
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ the `colorArg` key is for whether you want the `--color=always` arg in your `git
|
|||||||
git:
|
git:
|
||||||
paging:
|
paging:
|
||||||
colorArg: always
|
colorArg: always
|
||||||
pager: delta --dark --paging=never --24-bit-color=never
|
pager: delta --dark --paging=never
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Global Keybindings
|
## Global Keybindings
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||||
<kbd>m</kbd>: view merge/rebase options
|
<kbd>m</kbd>: view merge/rebase options
|
||||||
@@ -16,9 +17,10 @@
|
|||||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
<kbd>_</kbd>: prev screen mode
|
<kbd>_</kbd>: prev screen mode
|
||||||
<kbd>:</kbd>: execute custom command
|
<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>W</kbd>: open diff menu
|
||||||
<kbd>ctrl+e</kbd>: open diff menu
|
<kbd>ctrl+e</kbd>: open diff menu
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## List Panel Navigation
|
## List Panel Navigation
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
<pre>
|
<pre>
|
||||||
<kbd>space</kbd>: checkout
|
<kbd>space</kbd>: checkout
|
||||||
<kbd>o</kbd>: create pull request
|
<kbd>o</kbd>: create pull request
|
||||||
|
<kbd>O</kbd>: create pull request options
|
||||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||||
<kbd>c</kbd>: checkout by name
|
<kbd>c</kbd>: checkout by name
|
||||||
<kbd>F</kbd>: force checkout
|
<kbd>F</kbd>: force checkout
|
||||||
@@ -109,7 +112,8 @@
|
|||||||
<kbd>o</kbd>: open file
|
<kbd>o</kbd>: open file
|
||||||
<kbd>e</kbd>: edit file
|
<kbd>e</kbd>: edit file
|
||||||
<kbd>space</kbd>: toggle file included in patch
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Commits Panel (Commits)
|
## Commits Panel (Commits)
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
<kbd>g</kbd>: reset to this commit
|
<kbd>g</kbd>: reset to this commit
|
||||||
<kbd>f</kbd>: fixup commit
|
<kbd>f</kbd>: fixup commit
|
||||||
<kbd>F</kbd>: create fixup commit for this 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>d</kbd>: delete commit
|
||||||
<kbd>ctrl+j</kbd>: move commit down one
|
<kbd>ctrl+j</kbd>: move commit down one
|
||||||
<kbd>ctrl+k</kbd>: move commit up one
|
<kbd>ctrl+k</kbd>: move commit up one
|
||||||
@@ -153,6 +157,12 @@
|
|||||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Extras Panel
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
|
</pre>
|
||||||
|
|
||||||
## Files Panel (Files)
|
## Files Panel (Files)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
@@ -170,10 +180,13 @@
|
|||||||
<kbd>S</kbd>: view stash options
|
<kbd>S</kbd>: view stash options
|
||||||
<kbd>a</kbd>: stage/unstage all
|
<kbd>a</kbd>: stage/unstage all
|
||||||
<kbd>D</kbd>: view reset options
|
<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>f</kbd>: fetch
|
||||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||||
<kbd>g</kbd>: view upstream reset options
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Files Panel (Submodules)
|
## Files Panel (Submodules)
|
||||||
@@ -193,20 +206,21 @@
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: return to files panel
|
<kbd>esc</kbd>: return to files panel
|
||||||
|
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||||
<kbd>space</kbd>: pick hunk
|
<kbd>space</kbd>: pick hunk
|
||||||
<kbd>b</kbd>: pick both hunks
|
<kbd>b</kbd>: pick all hunks
|
||||||
<kbd>◄</kbd>: select previous conflict
|
<kbd>◄</kbd>: select previous conflict
|
||||||
<kbd>►</kbd>: select next conflict
|
<kbd>►</kbd>: select next conflict
|
||||||
<kbd>▲</kbd>: select top hunk
|
<kbd>▲</kbd>: select previous hunk
|
||||||
<kbd>▼</kbd>: select bottom hunk
|
<kbd>▼</kbd>: select next hunk
|
||||||
<kbd>z</kbd>: undo
|
<kbd>z</kbd>: undo
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Main Panel (Normal)
|
## Main Panel (Normal)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||||
<kbd>¦</kbd>: scroll up (fn+down)
|
<kbd>ő</kbd>: scroll up (fn+down)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Main Panel (Patch Building)
|
## Main Panel (Patch Building)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Lazygit Sneltoetsen
|
# Lazygit Sneltoetsen
|
||||||
|
|
||||||
## Globaale Sneltoetsen
|
## Globale Sneltoetsen
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
|
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
|
||||||
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
|
<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>m</kbd>: bekijk merge/rebase opties
|
||||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||||
<kbd>P</kbd>: push
|
<kbd>P</kbd>: push
|
||||||
@@ -13,31 +14,33 @@
|
|||||||
<kbd>x</kbd>: open menu
|
<kbd>x</kbd>: open menu
|
||||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||||
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
|
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||||
<kbd>_</kbd>: vorige schermmode
|
<kbd>_</kbd>: vorige scherm modus
|
||||||
<kbd>:</kbd>: voor aangepast commando uit
|
<kbd>:</kbd>: voor aangepaste commando uit
|
||||||
<kbd>|</kbd>: bekijk scoping opties
|
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||||
<kbd>W</kbd>: open diff menu
|
<kbd>W</kbd>: open diff menu
|
||||||
<kbd>ctrl+e</kbd>: open diff menu
|
<kbd>ctrl+e</kbd>: open diff menu
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## List Panel Navigation
|
## Lijstpaneel Navigatie
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>.</kbd>: volgende pagina
|
<kbd>.</kbd>: volgende pagina
|
||||||
<kbd>,</kbd>: vorige pagina
|
<kbd>,</kbd>: vorige pagina
|
||||||
<kbd><</kbd>: scroll naar boven
|
<kbd><</kbd>: scroll naar boven
|
||||||
<kbd>></kbd>: scroll naar beneden
|
<kbd>></kbd>: scroll naar beneden
|
||||||
<kbd>/</kbd>: start met zoekken
|
<kbd>/</kbd>: start met zoeken
|
||||||
<kbd>]</kbd>: volgende tab
|
<kbd>]</kbd>: volgende tabblad
|
||||||
<kbd>[</kbd>: vorige tab
|
<kbd>[</kbd>: vorige tabblad
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Branches Paneel (Branches Tab)
|
## Branches Paneel (Branches Tabblad)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>space</kbd>: uitchecken
|
<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>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||||
<kbd>c</kbd>: uitchecken bij naam
|
<kbd>c</kbd>: uitchecken bij naam
|
||||||
<kbd>F</kbd>: forceer checkout
|
<kbd>F</kbd>: forceer checkout
|
||||||
@@ -49,16 +52,16 @@
|
|||||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||||
<kbd>g</kbd>: bekijk reset opties
|
<kbd>g</kbd>: bekijk reset opties
|
||||||
<kbd>R</kbd>: hernoem branch
|
<kbd>R</kbd>: hernoem branch
|
||||||
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
|
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||||
<kbd>enter</kbd>: view commits
|
<kbd>enter</kbd>: bekijk commits
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
## Branches Paneel (Remote Branches (in Remotes tabblad))
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||||
<kbd>g</kbd>: bekijk reset opties
|
<kbd>g</kbd>: bekijk reset opties
|
||||||
<kbd>enter</kbd>: view commits
|
<kbd>enter</kbd>: bekijk commits
|
||||||
<kbd>space</kbd>: uitchecken
|
<kbd>space</kbd>: uitchecken
|
||||||
<kbd>n</kbd>: nieuwe branch
|
<kbd>n</kbd>: nieuwe branch
|
||||||
<kbd>M</kbd>: merge in met huidige checked out 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
|
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Branches Paneel (Remotes Tab)
|
## Branches Paneel (Remotes Tabblad)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>f</kbd>: fetch remote
|
<kbd>f</kbd>: fetch remote
|
||||||
@@ -83,13 +86,13 @@
|
|||||||
<kbd>space</kbd>: checkout commit
|
<kbd>space</kbd>: checkout commit
|
||||||
<kbd>g</kbd>: bekijk reset opties
|
<kbd>g</kbd>: bekijk reset opties
|
||||||
<kbd>n</kbd>: nieuwe branch
|
<kbd>n</kbd>: nieuwe branch
|
||||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Branches Paneel (Tags Tab)
|
## Branches Paneel (Tags Tabblad)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>space</kbd>: uitchecken
|
<kbd>space</kbd>: uitchecken
|
||||||
@@ -97,7 +100,7 @@
|
|||||||
<kbd>P</kbd>: push tag
|
<kbd>P</kbd>: push tag
|
||||||
<kbd>n</kbd>: creëer tag
|
<kbd>n</kbd>: creëer tag
|
||||||
<kbd>g</kbd>: bekijk reset opties
|
<kbd>g</kbd>: bekijk reset opties
|
||||||
<kbd>enter</kbd>: view commits
|
<kbd>enter</kbd>: bekijk commits
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Commit bestanden Paneel
|
## Commit bestanden Paneel
|
||||||
@@ -109,7 +112,8 @@
|
|||||||
<kbd>o</kbd>: open bestand
|
<kbd>o</kbd>: open bestand
|
||||||
<kbd>e</kbd>: verander bestand
|
<kbd>e</kbd>: verander bestand
|
||||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Commits Paneel (Commits)
|
## Commits Paneel (Commits)
|
||||||
@@ -129,28 +133,34 @@
|
|||||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||||
<kbd>t</kbd>: commit ongedaan maken
|
<kbd>t</kbd>: commit ongedaan maken
|
||||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||||
<kbd>space</kbd>: checkout commit
|
<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>T</kbd>: tag commit
|
||||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||||
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
|
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Commits Paneel (Reflog Tab)
|
## Commits Paneel (Reflog Tabblad)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||||
<kbd>space</kbd>: checkout commit
|
<kbd>space</kbd>: checkout commit
|
||||||
<kbd>g</kbd>: bekijk reset opties
|
<kbd>g</kbd>: bekijk reset opties
|
||||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Extras Paneel
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Bestanden Paneel (Bestanden)
|
## Bestanden Paneel (Bestanden)
|
||||||
@@ -174,25 +184,29 @@
|
|||||||
<kbd>f</kbd>: fetch
|
<kbd>f</kbd>: fetch
|
||||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||||
<kbd>g</kbd>: bekijk upstream reset opties
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Bestanden Paneel (Submodules)
|
## Bestanden Paneel (Submodules)
|
||||||
|
|
||||||
<pre>
|
<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>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>u</kbd>: update submodule
|
||||||
<kbd>n</kbd>: add new submodule
|
<kbd>n</kbd>: voeg nieuwe submodule toe
|
||||||
<kbd>e</kbd>: update submodule URL
|
<kbd>e</kbd>: update submodule URL
|
||||||
<kbd>i</kbd>: initialize submodule
|
<kbd>i</kbd>: initialiseer submodule
|
||||||
<kbd>b</kbd>: view bulk submodule options
|
<kbd>b</kbd>: bekijk bulk submodule opties
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Hooft Paneel (Merggen)
|
## Hoofd Paneel (Mergen)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||||
|
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||||
<kbd>space</kbd>: kies hunk
|
<kbd>space</kbd>: kies hunk
|
||||||
<kbd>b</kbd>: kies bijde hunks
|
<kbd>b</kbd>: kies bijde hunks
|
||||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||||
@@ -202,29 +216,29 @@
|
|||||||
<kbd>z</kbd>: ongedaan maken
|
<kbd>z</kbd>: ongedaan maken
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Hooft Paneel (Normaal)
|
## Hoofd Paneel (Normaal)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd> ̄</kbd>: scroll omlaag (fn+up)
|
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||||
<kbd>¦</kbd>: scroll omhoog (fn+down)
|
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Hooft Paneel (Patch Bouwen)
|
## Hoofd Paneel (Patch Bouwen)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||||
<kbd>o</kbd>: open bestand
|
<kbd>o</kbd>: open bestand
|
||||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||||
<kbd>►</kbd>: selecteer de volgende hunk
|
<kbd>►</kbd>: selecteer de volgende hunk
|
||||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||||
<kbd>v</kbd>: toggle drag selecteer
|
<kbd>v</kbd>: toggle drag selecteer
|
||||||
<kbd>V</kbd>: toggle drag selecteer
|
<kbd>V</kbd>: toggle drag selecteer
|
||||||
<kbd>a</kbd>: toggle selecteer hunk
|
<kbd>a</kbd>: toggle selecteer hunk
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Hooft Paneel (Staging)
|
## Hoofd Paneel (Staging)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||||
@@ -238,9 +252,9 @@
|
|||||||
<kbd>►</kbd>: selecteer de volgende hunk
|
<kbd>►</kbd>: selecteer de volgende hunk
|
||||||
<kbd>e</kbd>: verander bestand
|
<kbd>e</kbd>: verander bestand
|
||||||
<kbd>o</kbd>: open bestand
|
<kbd>o</kbd>: open bestand
|
||||||
<kbd>v</kbd>: toggle drag selecteer
|
<kbd>v</kbd>: toggle drag selecteer
|
||||||
<kbd>V</kbd>: toggle drag selecteer
|
<kbd>V</kbd>: toggle drag selecteer
|
||||||
<kbd>a</kbd>: toggle selecteer hunk
|
<kbd>a</kbd>: toggle selecteer hunk
|
||||||
<kbd>c</kbd>: Commit veranderingen
|
<kbd>c</kbd>: Commit veranderingen
|
||||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||||
@@ -255,7 +269,7 @@
|
|||||||
## Stash Paneel
|
## Stash Paneel
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>enter</kbd>: view stash entry's files
|
<kbd>enter</kbd>: bekijk bestanden van stash entry
|
||||||
<kbd>space</kbd>: toepassen
|
<kbd>space</kbd>: toepassen
|
||||||
<kbd>g</kbd>: pop
|
<kbd>g</kbd>: pop
|
||||||
<kbd>d</kbd>: laten vallen
|
<kbd>d</kbd>: laten vallen
|
||||||
@@ -269,5 +283,5 @@
|
|||||||
<kbd>o</kbd>: open config bestand
|
<kbd>o</kbd>: open config bestand
|
||||||
<kbd>u</kbd>: check voor updates
|
<kbd>u</kbd>: check voor updates
|
||||||
<kbd>enter</kbd>: wissel naar een recente repo
|
<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>
|
</pre>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Globalne
|
## Globalne
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
|
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||||
<kbd>m</kbd>: view merge/rebase options
|
<kbd>m</kbd>: view merge/rebase options
|
||||||
@@ -16,9 +17,10 @@
|
|||||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||||
<kbd>_</kbd>: prev screen mode
|
<kbd>_</kbd>: prev screen mode
|
||||||
<kbd>:</kbd>: execute custom command
|
<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>W</kbd>: open diff menu
|
||||||
<kbd>ctrl+e</kbd>: open diff menu
|
<kbd>ctrl+e</kbd>: open diff menu
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## List Panel Navigation
|
## List Panel Navigation
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
<pre>
|
<pre>
|
||||||
<kbd>space</kbd>: przełącz
|
<kbd>space</kbd>: przełącz
|
||||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
<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>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
|
||||||
<kbd>c</kbd>: przełącz używając nazwy
|
<kbd>c</kbd>: przełącz używając nazwy
|
||||||
<kbd>F</kbd>: wymuś przełączenie
|
<kbd>F</kbd>: wymuś przełączenie
|
||||||
@@ -109,7 +112,8 @@
|
|||||||
<kbd>o</kbd>: otwórz plik
|
<kbd>o</kbd>: otwórz plik
|
||||||
<kbd>e</kbd>: edytuj plik
|
<kbd>e</kbd>: edytuj plik
|
||||||
<kbd>space</kbd>: toggle file included in patch
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Commity Panel (Commity)
|
## Commity Panel (Commity)
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
<kbd>g</kbd>: zresetuj do tego commita
|
<kbd>g</kbd>: zresetuj do tego commita
|
||||||
<kbd>f</kbd>: napraw commit
|
<kbd>f</kbd>: napraw commit
|
||||||
<kbd>F</kbd>: create fixup commit for this 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>d</kbd>: delete commit
|
||||||
<kbd>ctrl+j</kbd>: move commit down one
|
<kbd>ctrl+j</kbd>: move commit down one
|
||||||
<kbd>ctrl+k</kbd>: move commit up one
|
<kbd>ctrl+k</kbd>: move commit up one
|
||||||
@@ -153,6 +157,12 @@
|
|||||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
|
## Extras Panel
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<kbd>@</kbd>: open command log menu
|
||||||
|
</pre>
|
||||||
|
|
||||||
## Pliki Panel (Pliki)
|
## Pliki Panel (Pliki)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
@@ -174,6 +184,9 @@
|
|||||||
<kbd>f</kbd>: fetch
|
<kbd>f</kbd>: fetch
|
||||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||||
<kbd>g</kbd>: view upstream reset options
|
<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>
|
</pre>
|
||||||
|
|
||||||
## Pliki Panel (Submodules)
|
## Pliki Panel (Submodules)
|
||||||
@@ -193,20 +206,21 @@
|
|||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd>esc</kbd>: wróć do panelu plików
|
<kbd>esc</kbd>: wróć do panelu plików
|
||||||
|
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||||
<kbd>space</kbd>: pick hunk
|
<kbd>space</kbd>: pick hunk
|
||||||
<kbd>b</kbd>: pick both hunks
|
<kbd>b</kbd>: pick all hunks
|
||||||
<kbd>◄</kbd>: select previous conflict
|
<kbd>◄</kbd>: select previous conflict
|
||||||
<kbd>►</kbd>: select next conflict
|
<kbd>►</kbd>: select next conflict
|
||||||
<kbd>▲</kbd>: select top hunk
|
<kbd>▲</kbd>: select previous hunk
|
||||||
<kbd>▼</kbd>: select bottom hunk
|
<kbd>▼</kbd>: select next hunk
|
||||||
<kbd>z</kbd>: cofnij
|
<kbd>z</kbd>: cofnij
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Main Panel (Normal)
|
## Main Panel (Normal)
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||||
<kbd>¦</kbd>: scroll up (fn+down)
|
<kbd>ő</kbd>: scroll up (fn+down)
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
## Main Panel (Patch Building)
|
## Main Panel (Patch Building)
|
||||||
|
|||||||
@@ -9,36 +9,39 @@ require (
|
|||||||
github.com/cli/safeexec v1.0.0
|
github.com/cli/safeexec v1.0.0
|
||||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||||
github.com/creack/pty v1.1.11
|
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/fsnotify/fsnotify v1.4.7
|
||||||
github.com/go-errors/errors v1.1.1
|
github.com/go-errors/errors v1.4.1
|
||||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||||
github.com/golang/protobuf v1.3.2 // indirect
|
github.com/golang/protobuf v1.3.2 // indirect
|
||||||
github.com/google/go-cmp v0.3.1 // indirect
|
github.com/google/go-cmp v0.5.6 // indirect
|
||||||
|
github.com/gookit/color v1.4.2
|
||||||
github.com/imdario/mergo v0.3.11
|
github.com/imdario/mergo v0.3.11
|
||||||
github.com/integrii/flaggy v1.4.0
|
github.com/integrii/flaggy v1.4.0
|
||||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
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/gocui v0.3.1-0.20211102104458-40df0be5a474
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
|
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
github.com/kyokomi/emoji/v2 v2.2.8
|
||||||
github.com/mattn/go-runewidth v0.0.10
|
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||||
|
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.13
|
||||||
github.com/mgutz/str v1.2.0
|
github.com/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/ginkgo v1.10.3 // indirect
|
||||||
github.com/onsi/gomega v1.7.1 // 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/sahilm/fuzzy v0.1.0
|
||||||
github.com/sirupsen/logrus v1.4.2
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // 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-20211102061401-a2f17f7b995c // indirect
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
|
||||||
|
|||||||
@@ -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/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 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
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.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 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
@@ -31,31 +30,38 @@ 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/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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||||
|
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
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.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
||||||
|
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
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 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
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 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-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.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 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||||
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||||
|
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
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.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
@@ -65,14 +71,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
|||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/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 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/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.20211102104458-40df0be5a474 h1:4H/oJcUmwJpqyXzqfn+lsjQ/bjpm/HszzLrVbCjgqj4=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
|
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
|
||||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
|
||||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
|
||||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
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/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
@@ -85,38 +87,36 @@ 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/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 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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.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.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.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.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.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
|
||||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
|
||||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.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 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
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 h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
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.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 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
||||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -126,7 +126,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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
@@ -142,21 +141,24 @@ 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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/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 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
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-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-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-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-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 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
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-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-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-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 h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
|
||||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
|
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
|
||||||
@@ -169,24 +171,33 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201005172224-997123666555/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
|
||||||
|
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 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-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 h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
|
||||||
|
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
@@ -195,3 +206,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.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
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.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=
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/integrii/flaggy"
|
"github.com/integrii/flaggy"
|
||||||
"github.com/jesseduffield/lazygit/pkg/app"
|
"github.com/jesseduffield/lazygit/pkg/app"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||||
"github.com/jesseduffield/lazygit/pkg/env"
|
"github.com/jesseduffield/lazygit/pkg/env"
|
||||||
yaml "github.com/jesseduffield/yaml"
|
yaml "github.com/jesseduffield/yaml"
|
||||||
)
|
)
|
||||||
@@ -60,6 +61,9 @@ func main() {
|
|||||||
gitDir := ""
|
gitDir := ""
|
||||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||||
|
|
||||||
|
customConfig := ""
|
||||||
|
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma seperated list to custom config file(s)")
|
||||||
|
|
||||||
flaggy.Parse()
|
flaggy.Parse()
|
||||||
|
|
||||||
if repoPath != "" {
|
if repoPath != "" {
|
||||||
@@ -71,6 +75,10 @@ func main() {
|
|||||||
gitDir = filepath.Join(repoPath, ".git")
|
gitDir = filepath.Join(repoPath, ".git")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if customConfig != "" {
|
||||||
|
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||||
|
}
|
||||||
|
|
||||||
if useConfigDir != "" {
|
if useConfigDir != "" {
|
||||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||||
}
|
}
|
||||||
@@ -134,6 +142,6 @@ func main() {
|
|||||||
stackTrace := newErr.ErrorStack()
|
stackTrace := newErr.ErrorStack()
|
||||||
app.Log.Error(stackTrace)
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-23
@@ -15,12 +15,12 @@ import (
|
|||||||
|
|
||||||
"github.com/aybabtme/humanlog"
|
"github.com/aybabtme/humanlog"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/env"
|
"github.com/jesseduffield/lazygit/pkg/env"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -97,13 +97,17 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
|||||||
|
|
||||||
// NewApp bootstrap a new application
|
// NewApp bootstrap a new application
|
||||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
closers: []io.Closer{},
|
closers: []io.Closer{},
|
||||||
Config: config,
|
Config: config,
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
app.Log = newLogger(config)
|
app.Log = newLogger(config)
|
||||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
|
||||||
|
if err != nil {
|
||||||
|
return app, err
|
||||||
|
}
|
||||||
|
|
||||||
// if we are being called in 'demon' mode, we can just return here
|
// if we are being called in 'demon' mode, we can just return here
|
||||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||||
@@ -123,7 +127,13 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
|||||||
return app, err
|
return app, err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
|
app.GitCommand, err = commands.NewGitCommand(
|
||||||
|
app.Log,
|
||||||
|
app.OSCommand,
|
||||||
|
app.Tr,
|
||||||
|
app.Config,
|
||||||
|
git_config.NewStdCachedGitConfig(app.Log),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return app, err
|
return app, err
|
||||||
}
|
}
|
||||||
@@ -182,7 +192,7 @@ func (app *App) setupRepo() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if we are not in a git repo, we ask if we want to `git init`
|
// 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()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -225,6 +235,7 @@ func (app *App) setupRepo() (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +248,7 @@ func (app *App) Run() error {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.Gui.RunWithSubprocesses()
|
err := app.Gui.RunAndHandleError()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +327,9 @@ func TailLogs() {
|
|||||||
|
|
||||||
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
||||||
|
|
||||||
|
opts := humanlog.DefaultOptions
|
||||||
|
opts.Truncates = false
|
||||||
|
|
||||||
_, err = os.Stat(logFilePath)
|
_, err = os.Stat(logFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -324,22 +338,5 @@ func TailLogs() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
TailLogsForPlatform(logFilePath, opts)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +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
|
||||||
|
}
|
||||||
+20
-16
@@ -11,19 +11,19 @@ import (
|
|||||||
|
|
||||||
// NewBranch create new branch
|
// NewBranch create new branch
|
||||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
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", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentBranchName get the current branch name and displayname.
|
// CurrentBranchName get the current branch name and displayname.
|
||||||
// the first returned string is the name and the second is the 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)'
|
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
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" {
|
if err == nil && branchName != "HEAD\n" {
|
||||||
trimmedBranchName := strings.TrimSpace(branchName)
|
trimmedBranchName := strings.TrimSpace(branchName)
|
||||||
return trimmedBranchName, trimmedBranchName, nil
|
return trimmedBranchName, trimmedBranchName, nil
|
||||||
}
|
}
|
||||||
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
|
output, err := c.RunCommandWithOutput("git branch --contains")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
|||||||
command = "git branch -D"
|
command = "git branch -D"
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.OSCommand.RunCommand("%s %s", command, branch)
|
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||||
@@ -59,9 +59,9 @@ type CheckoutOptions struct {
|
|||||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||||
forceArg := ""
|
forceArg := ""
|
||||||
if options.Force {
|
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, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||||
@@ -73,24 +73,24 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
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}", c.OSCommand.Quote(branchName))
|
||||||
return strings.TrimSpace(output), err
|
return strings.TrimSpace(output), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||||
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
|
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
|
||||||
templateValues := map[string]string{
|
templateValues := map[string]string{
|
||||||
"branchName": branchName,
|
"branchName": c.OSCommand.Quote(branchName),
|
||||||
}
|
}
|
||||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||||
return c.OSCommand.RunCommand("git branch -u %s", upstream)
|
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
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", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||||
@@ -124,7 +124,7 @@ type MergeOpts struct {
|
|||||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||||
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
|
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
|
||||||
|
|
||||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
|
||||||
if opts.FastForwardOnly {
|
if opts.FastForwardOnly {
|
||||||
command = fmt.Sprintf("%s --ff-only", command)
|
command = fmt.Sprintf("%s --ff-only", command)
|
||||||
}
|
}
|
||||||
@@ -134,24 +134,28 @@ func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
|||||||
|
|
||||||
// AbortMerge abort merge
|
// AbortMerge abort merge
|
||||||
func (c *GitCommand) AbortMerge() error {
|
func (c *GitCommand) AbortMerge() error {
|
||||||
return c.OSCommand.RunCommand("git merge --abort")
|
return c.RunCommand("git merge --abort")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) IsHeadDetached() bool {
|
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
|
return err != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetHardHead runs `git reset --hard`
|
// ResetHardHead runs `git reset --hard`
|
||||||
func (c *GitCommand) ResetHard(ref string) error {
|
func (c *GitCommand) ResetHard(ref string) error {
|
||||||
return c.OSCommand.RunCommand("git reset --hard " + ref)
|
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetSoft runs `git reset --soft HEAD`
|
// ResetSoft runs `git reset --soft HEAD`
|
||||||
func (c *GitCommand) ResetSoft(ref string) error {
|
func (c *GitCommand) ResetSoft(ref string) error {
|
||||||
return c.OSCommand.RunCommand("git reset --soft " + ref)
|
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) ResetMixed(ref string) error {
|
||||||
|
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
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", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-21
@@ -2,7 +2,6 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
@@ -11,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// RenameCommit renames the topmost commit with the given name
|
// RenameCommit renames the topmost commit with the given name
|
||||||
func (c *GitCommand) RenameCommit(name string) error {
|
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
|
// ResetToCommit reset to commit
|
||||||
@@ -19,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)
|
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit commits to git
|
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
|
||||||
splitMessage := strings.Split(message, "\n")
|
splitMessage := strings.Split(message, "\n")
|
||||||
lineArgs := ""
|
lineArgs := ""
|
||||||
for _, line := range splitMessage {
|
for _, line := range splitMessage {
|
||||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
flagsStr := ""
|
||||||
if c.usingGpg() {
|
if flags != "" {
|
||||||
return c.OSCommand.ShellCommandFromString(command), nil
|
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
|
// Get the subject of the HEAD commit
|
||||||
@@ -49,19 +47,17 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
|||||||
return strings.TrimSpace(message), err
|
return strings.TrimSpace(message), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
|
||||||
command := "git commit --amend --no-edit --allow-empty"
|
|
||||||
if c.usingGpg() {
|
|
||||||
return c.OSCommand.ShellCommandFromString(command), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, c.OSCommand.RunCommand(command)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
func (c *GitCommand) AmendHead() error {
|
||||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
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 {
|
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||||
@@ -74,7 +70,11 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
|||||||
|
|
||||||
// Revert reverts the selected commit by sha
|
// Revert reverts the selected commit by sha
|
||||||
func (c *GitCommand) Revert(sha string) error {
|
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
|
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||||
@@ -94,5 +94,5 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
|
|||||||
|
|
||||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||||
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
|
return c.RunCommand("git commit --fixup=%s", sha)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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) {
|
||||||
|
gitCmd := NewDummyGitCommand()
|
||||||
|
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
message string
|
||||||
|
flags string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
testName: "Commit",
|
||||||
|
message: "test",
|
||||||
|
flags: "",
|
||||||
|
expected: "git commit -m " + gitCmd.OSCommand.Quote("test"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Commit with --no-verify flag",
|
||||||
|
message: "test",
|
||||||
|
flags: "--no-verify",
|
||||||
|
expected: "git commit --no-verify -m " + gitCmd.OSCommand.Quote("test"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Commit with multiline message",
|
||||||
|
message: "line1\nline2",
|
||||||
|
flags: "",
|
||||||
|
expected: "git commit -m " + gitCmd.OSCommand.Quote("line1") + " -m " + gitCmd.OSCommand.Quote("line2"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-9
@@ -15,12 +15,8 @@ func (c *GitCommand) ConfiguredPager() string {
|
|||||||
if os.Getenv("PAGER") != "" {
|
if os.Getenv("PAGER") != "" {
|
||||||
return os.Getenv("PAGER")
|
return os.Getenv("PAGER")
|
||||||
}
|
}
|
||||||
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
|
output := c.GitConfig.Get("core.pager")
|
||||||
if err != nil {
|
return strings.Split(output, "\n")[0]
|
||||||
return ""
|
|
||||||
}
|
|
||||||
trimmedOutput := strings.TrimSpace(output)
|
|
||||||
return strings.Split(trimmedOutput, "\n")[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) GetPager(width int) string {
|
func (c *GitCommand) GetPager(width int) string {
|
||||||
@@ -42,7 +38,13 @@ func (c *GitCommand) colorArg() string {
|
|||||||
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) GetConfigValue(key string) string {
|
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||||
output, _ := c.getGitConfigValue(key)
|
// whether we need to run a subprocess to allow them to enter their password
|
||||||
return output
|
func (c *GitCommand) UsingGpg() bool {
|
||||||
|
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||||
|
if overrideGpg {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.GitConfig.GetBool("commit.gpgsign")
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-6
@@ -1,6 +1,10 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
@@ -14,12 +18,13 @@ func NewDummyGitCommand() *GitCommand {
|
|||||||
|
|
||||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||||
|
newAppConfig := config.NewDummyAppConfig()
|
||||||
return &GitCommand{
|
return &GitCommand{
|
||||||
Log: utils.NewDummyLog(),
|
Log: utils.NewDummyLog(),
|
||||||
OSCommand: osCommand,
|
OSCommand: osCommand,
|
||||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
|
||||||
Config: config.NewDummyAppConfig(),
|
Config: newAppConfig,
|
||||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
|
||||||
removeFile: func(string) error { return nil },
|
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-51
@@ -2,48 +2,59 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/mgutz/str"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CatFile obtains the content of a file
|
// CatFile obtains the content of a file
|
||||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||||
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
|
buf, err := ioutil.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||||
|
return "git mergetool"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) OpenMergeTool() error {
|
||||||
|
return c.OSCommand.RunCommand("git mergetool")
|
||||||
}
|
}
|
||||||
|
|
||||||
// StageFile stages a file
|
// StageFile stages a file
|
||||||
func (c *GitCommand) StageFile(fileName string) error {
|
func (c *GitCommand) StageFile(fileName string) error {
|
||||||
// renamed files look like "file1 -> file2"
|
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||||
fileNames := strings.Split(fileName, " -> ")
|
|
||||||
return c.OSCommand.RunCommand("git add -- %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StageAll stages all files
|
// StageAll stages all files
|
||||||
func (c *GitCommand) StageAll() error {
|
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 {
|
func (c *GitCommand) UnstageAll() error {
|
||||||
return c.OSCommand.RunCommand("git reset")
|
return c.RunCommand("git reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnStageFile unstages a file
|
// UnStageFile unstages a file
|
||||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
// 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"
|
command := "git rm --cached --force -- %s"
|
||||||
if tracked {
|
if reset {
|
||||||
command = "git reset HEAD -- %s"
|
command = "git reset HEAD -- %s"
|
||||||
}
|
}
|
||||||
|
|
||||||
// renamed files look like "file1 -> file2"
|
|
||||||
fileNames := strings.Split(fileName, " -> ")
|
|
||||||
for _, name := range fileNames {
|
for _, name := range fileNames {
|
||||||
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -58,20 +69,19 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
|
|||||||
return nil, nil, errors.New("Expected renamed file")
|
return nil, nil, errors.New("Expected renamed file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// we've got a file that represents a rename from one file to another. Unfortunately
|
// we've got a file that represents a rename from one file to another. Here we will refetch
|
||||||
// our File abstraction fails to consider this case, so here we will refetch
|
|
||||||
// all files, passing the --no-renames flag and then recursively call the function
|
// 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})
|
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||||
var beforeFile *models.File
|
var beforeFile *models.File
|
||||||
var afterFile *models.File
|
var afterFile *models.File
|
||||||
for _, f := range filesWithoutRenames {
|
for _, f := range filesWithoutRenames {
|
||||||
if f.Name == split[0] {
|
if f.Name == file.PreviousName {
|
||||||
beforeFile = f
|
beforeFile = f
|
||||||
}
|
}
|
||||||
if f.Name == split[1] {
|
|
||||||
|
if f.Name == file.Name {
|
||||||
afterFile = f
|
afterFile = f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,24 +117,76 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the file isn't tracked, we assume you want to delete it
|
|
||||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
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 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !file.Tracked {
|
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
|
||||||
return c.removeFile(file.Name)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.Added {
|
||||||
|
return c.OSCommand.RemoveFile(file.Name)
|
||||||
}
|
}
|
||||||
return c.DiscardUnstagedFileChanges(file)
|
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
|
// DiscardUnstagedFileChanges directly
|
||||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
||||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
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
|
// Ignore adds a file to the gitignore for the repo
|
||||||
@@ -133,33 +195,36 @@ func (c *GitCommand) Ignore(filename string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WorktreeFileDiff returns the diff of a file
|
// 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
|
// 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
|
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 := ""
|
cachedArg := ""
|
||||||
trackedArg := "--"
|
trackedArg := "--"
|
||||||
colorArg := c.colorArg()
|
colorArg := c.colorArg()
|
||||||
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
|
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||||
fileName := c.OSCommand.Quote(split[len(split)-1])
|
ignoreWhitespaceArg := ""
|
||||||
if cached {
|
if cached {
|
||||||
cachedArg = "--cached"
|
cachedArg = "--cached"
|
||||||
}
|
}
|
||||||
if !file.Tracked && !file.HasStagedChanges && !cached {
|
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
|
||||||
trackedArg = "--no-index -- /dev/null"
|
trackedArg = "--no-index -- /dev/null"
|
||||||
}
|
}
|
||||||
if plain {
|
if plain {
|
||||||
colorArg = "never"
|
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, quotedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||||
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||||
c.Log.Infof("saving temporary patch to %s", filepath)
|
c.Log.Infof("saving temporary patch to %s", filepath)
|
||||||
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
|
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -170,7 +235,7 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
|||||||
flagStr += " --" + flag
|
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
|
// 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 +256,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
|
|||||||
reverseFlag = " -R "
|
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
|
// CheckoutFile checks out the file for the given commit
|
||||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
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
|
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||||
@@ -206,7 +271,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
// 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", c.OSCommand.Quote(fileName)); err != nil {
|
||||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -218,10 +283,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the commit
|
// amend the commit
|
||||||
cmd, err := c.AmendHead()
|
err := c.AmendHead()
|
||||||
if cmd != nil {
|
|
||||||
return errors.New("received unexpected pointer to cmd")
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -232,17 +294,17 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
|||||||
|
|
||||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
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
|
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
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", c.OSCommand.Quote(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUntrackedFiles runs `git clean -fd`
|
// RemoveUntrackedFiles runs `git clean -fd`
|
||||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
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
|
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||||
@@ -265,11 +327,16 @@ func (c *GitCommand) ResetAndClean() error {
|
|||||||
return c.RemoveUntrackedFiles()
|
return c.RemoveUntrackedFiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
|
||||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||||
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
|
||||||
editor := c.GetConfigValue("core.editor")
|
|
||||||
|
|
||||||
|
if editor == "" {
|
||||||
|
editor = c.GitConfig.Get("core.editor")
|
||||||
|
}
|
||||||
|
|
||||||
|
if editor == "" {
|
||||||
|
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||||
|
}
|
||||||
if editor == "" {
|
if editor == "" {
|
||||||
editor = c.OSCommand.Getenv("VISUAL")
|
editor = c.OSCommand.Getenv("VISUAL")
|
||||||
}
|
}
|
||||||
@@ -282,10 +349,15 @@ func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if editor == "" {
|
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)))
|
templateValues := map[string]string{
|
||||||
|
"editor": editor,
|
||||||
|
"filename": c.OSCommand.Quote(filename),
|
||||||
|
"line": strconv.Itoa(lineNumber),
|
||||||
|
}
|
||||||
|
|
||||||
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
|
||||||
|
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,878 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/test"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
gitConfigMockResponses map[string]string
|
||||||
|
commits []*models.Commit
|
||||||
|
commitIndex int
|
||||||
|
fileName string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"returns error when index outside of range of commits",
|
||||||
|
nil,
|
||||||
|
[]*models.Commit{},
|
||||||
|
0,
|
||||||
|
"test999.txt",
|
||||||
|
nil,
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"returns error when using gpg",
|
||||||
|
map[string]string{"commit.gpgsign": "true"},
|
||||||
|
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||||
|
0,
|
||||||
|
"test999.txt",
|
||||||
|
nil,
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"checks out file if it already existed",
|
||||||
|
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.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||||
|
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) {
|
||||||
|
gitCmd := NewDummyGitCommand()
|
||||||
|
|
||||||
|
type scenario struct {
|
||||||
|
filename string
|
||||||
|
configEditCommand string
|
||||||
|
configEditCommandTemplate string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
getenv func(string) string
|
||||||
|
gitConfigMockResponses map[string]string
|
||||||
|
test func(string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, args ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
map[string]string{"core.editor": "nano"},
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
if env == "VISUAL" {
|
||||||
|
return "nano"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
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 ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "emacs "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file/with space",
|
||||||
|
"",
|
||||||
|
"{{editor}} {{filename}}",
|
||||||
|
func(name string, args ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("file/with space"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"open file/at line",
|
||||||
|
"vim",
|
||||||
|
"{{editor}} +{{line}} {{filename}}",
|
||||||
|
func(name string, args ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "which", name)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(env string) string {
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
func(cmdStr string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "vim +1 "+gitCmd.OSCommand.Quote("open file/at line"), cmdStr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
|
||||||
|
gitCmd.Config.GetUserConfig().OS.EditCommandTemplate = s.configEditCommandTemplate
|
||||||
|
gitCmd.OSCommand.Command = s.command
|
||||||
|
gitCmd.OSCommand.Getenv = s.getenv
|
||||||
|
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||||
|
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
-26
@@ -1,14 +1,17 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
|
|
||||||
gogit "github.com/jesseduffield/go-git/v5"
|
gogit "github.com/jesseduffield/go-git/v5"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
@@ -31,37 +34,37 @@ type GitCommand struct {
|
|||||||
Repo *gogit.Repository
|
Repo *gogit.Repository
|
||||||
Tr *i18n.TranslationSet
|
Tr *i18n.TranslationSet
|
||||||
Config config.AppConfigurer
|
Config config.AppConfigurer
|
||||||
getGitConfigValue func(string) (string, error)
|
|
||||||
removeFile func(string) error
|
|
||||||
DotGitDir string
|
DotGitDir string
|
||||||
onSuccessfulContinue func() error
|
onSuccessfulContinue func() error
|
||||||
PatchManager *patch.PatchManager
|
PatchManager *patch.PatchManager
|
||||||
|
GitConfig git_config.IGitConfig
|
||||||
|
|
||||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||||
PushToCurrent bool
|
PushToCurrent bool
|
||||||
|
|
||||||
|
// this is just a view that we write to when running certain commands.
|
||||||
|
// Coincidentally at the moment it's the same view that OnRunCommand logs to
|
||||||
|
// but that need not always be the case.
|
||||||
|
GetCmdWriter func() io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGitCommand it runs git commands
|
// NewGitCommand it runs git commands
|
||||||
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
|
func NewGitCommand(
|
||||||
|
log *logrus.Entry,
|
||||||
|
osCommand *oscommands.OSCommand,
|
||||||
|
tr *i18n.TranslationSet,
|
||||||
|
config config.AppConfigurer,
|
||||||
|
gitConfig git_config.IGitConfig,
|
||||||
|
) (*GitCommand, error) {
|
||||||
var repo *gogit.Repository
|
var repo *gogit.Repository
|
||||||
|
|
||||||
// see what our default push behaviour is
|
pushToCurrent := gitConfig.Get("push.default") == "current"
|
||||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
|
||||||
pushToCurrent := false
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("error reading git config: %v", err)
|
|
||||||
} else {
|
|
||||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := verifyInGitRepo(osCommand.RunCommand); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -72,15 +75,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
|||||||
}
|
}
|
||||||
|
|
||||||
gitCommand := &GitCommand{
|
gitCommand := &GitCommand{
|
||||||
Log: log,
|
Log: log,
|
||||||
OSCommand: osCommand,
|
OSCommand: osCommand,
|
||||||
Tr: tr,
|
Tr: tr,
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Config: config,
|
Config: config,
|
||||||
getGitConfigValue: getGitConfigValue,
|
DotGitDir: dotGitDir,
|
||||||
removeFile: os.RemoveAll,
|
PushToCurrent: pushToCurrent,
|
||||||
DotGitDir: dotGitDir,
|
GitConfig: gitConfig,
|
||||||
PushToCurrent: pushToCurrent,
|
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||||
}
|
}
|
||||||
|
|
||||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||||
@@ -88,8 +91,25 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
|||||||
return gitCommand, nil
|
return gitCommand, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
|
func (c *GitCommand) WithSpan(span string) *GitCommand {
|
||||||
return runCmd("git status")
|
// 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 {
|
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||||
@@ -120,6 +140,18 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
|
|||||||
if err = chdir(".."); err != nil {
|
if err = chdir(".."); err != nil {
|
||||||
return utils.WrapError(err)
|
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 +221,40 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
|
||||||
|
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package git_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IGitConfig interface {
|
||||||
|
Get(string) string
|
||||||
|
GetBool(string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedGitConfig struct {
|
||||||
|
cache map[string]string
|
||||||
|
getKey func(string) (string, error)
|
||||||
|
log *logrus.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig {
|
||||||
|
return NewCachedGitConfig(getGitConfigValue, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCachedGitConfig(getKey func(string) (string, error), log *logrus.Entry) *CachedGitConfig {
|
||||||
|
return &CachedGitConfig{
|
||||||
|
cache: make(map[string]string),
|
||||||
|
getKey: getKey,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CachedGitConfig) Get(key string) string {
|
||||||
|
if value, ok := self.cache[key]; ok {
|
||||||
|
self.log.Debugf("using cache for key " + key)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
value := self.getAux(key)
|
||||||
|
self.cache[key] = value
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CachedGitConfig) getAux(key string) string {
|
||||||
|
value, err := self.getKey(key)
|
||||||
|
if err != nil {
|
||||||
|
self.log.Debugf("Error getting git config value for key: " + key + ". Error: " + err.Error())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CachedGitConfig) GetBool(key string) bool {
|
||||||
|
return isTruthy(self.Get(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTruthy(value string) bool {
|
||||||
|
lcValue := strings.ToLower(value)
|
||||||
|
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package git_config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetBool(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
mockResponses map[string]string
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"Option global and local config commit.gpgsign is not set",
|
||||||
|
map[string]string{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Some other random key is set",
|
||||||
|
map[string]string{"blah": "blah"},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Option commit.gpgsign is true",
|
||||||
|
map[string]string{"commit.gpgsign": "True"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Option commit.gpgsign is on",
|
||||||
|
map[string]string{"commit.gpgsign": "ON"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Option commit.gpgsign is yes",
|
||||||
|
map[string]string{"commit.gpgsign": "YeS"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Option commit.gpgsign is 1",
|
||||||
|
map[string]string{"commit.gpgsign": "1"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
s := s
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
fake := NewFakeGitConfig(s.mockResponses)
|
||||||
|
real := NewCachedGitConfig(
|
||||||
|
func(key string) (string, error) {
|
||||||
|
return fake.Get(key), nil
|
||||||
|
},
|
||||||
|
utils.NewDummyLog(),
|
||||||
|
)
|
||||||
|
result := real.GetBool("commit.gpgsign")
|
||||||
|
assert.Equal(t, s.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
mockResponses map[string]string
|
||||||
|
expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"not set",
|
||||||
|
map[string]string{},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"is set",
|
||||||
|
map[string]string{"commit.gpgsign": "blah"},
|
||||||
|
"blah",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
s := s
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
fake := NewFakeGitConfig(s.mockResponses)
|
||||||
|
real := NewCachedGitConfig(
|
||||||
|
func(key string) (string, error) {
|
||||||
|
return fake.Get(key), nil
|
||||||
|
},
|
||||||
|
utils.NewDummyLog(),
|
||||||
|
)
|
||||||
|
result := real.Get("commit.gpgsign")
|
||||||
|
assert.Equal(t, s.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifying that the cache is used
|
||||||
|
count := 0
|
||||||
|
real := NewCachedGitConfig(
|
||||||
|
func(key string) (string, error) {
|
||||||
|
count++
|
||||||
|
assert.Equal(t, "commit.gpgsign", key)
|
||||||
|
return "blah", nil
|
||||||
|
},
|
||||||
|
utils.NewDummyLog(),
|
||||||
|
)
|
||||||
|
result := real.Get("commit.gpgsign")
|
||||||
|
assert.Equal(t, "blah", result)
|
||||||
|
result = real.Get("commit.gpgsign")
|
||||||
|
assert.Equal(t, "blah", result)
|
||||||
|
assert.Equal(t, 1, count)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package git_config
|
||||||
|
|
||||||
|
type FakeGitConfig struct {
|
||||||
|
mockResponses map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeGitConfig(mockResponses map[string]string) *FakeGitConfig {
|
||||||
|
return &FakeGitConfig{
|
||||||
|
mockResponses: mockResponses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FakeGitConfig) Get(key string) string {
|
||||||
|
if self.mockResponses == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return self.mockResponses[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *FakeGitConfig) GetBool(key string) bool {
|
||||||
|
return isTruthy(self.Get(key))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package commands
|
package git_config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+4
-1961
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)
|
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/")
|
name := strings.TrimPrefix(split[1], "heads/")
|
||||||
branch := &models.Branch{
|
branch := &models.Branch{
|
||||||
|
|||||||
@@ -4,26 +4,25 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFilesInDiff get the specified commit files
|
// 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 := ""
|
reverseFlag := ""
|
||||||
if reverse {
|
if reverse {
|
||||||
reverseFlag = " -R "
|
reverseFlag = " -R "
|
||||||
}
|
}
|
||||||
|
|
||||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z %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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
|
return c.getCommitFilesFromFilenames(filenames), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// filenames string is something like "file1\nfile2\nfile3"
|
// 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)
|
commitFiles := make([]*models.CommitFile, 0)
|
||||||
|
|
||||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||||
@@ -32,16 +31,10 @@ func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string
|
|||||||
// typical result looks like 'A my_file' meaning my_file was added
|
// typical result looks like 'A my_file' meaning my_file was added
|
||||||
changeStatus := lines[i]
|
changeStatus := lines[i]
|
||||||
name := lines[i+1]
|
name := lines[i+1]
|
||||||
status := patch.UNSELECTED
|
|
||||||
if patchManager != nil && patchManager.To == parent {
|
|
||||||
status = patchManager.GetFileStatus(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
commitFiles = append(commitFiles, &models.CommitFile{
|
commitFiles = append(commitFiles, &models.CommitFile{
|
||||||
Parent: parent,
|
|
||||||
Name: name,
|
Name: name,
|
||||||
ChangeStatus: changeStatus,
|
ChangeStatus: changeStatus,
|
||||||
PatchStatus: status,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -37,7 +37,12 @@ type CommitListBuilder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCommitListBuilder builds a new commit list builder
|
// NewCommitListBuilder builds a new commit list builder
|
||||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
|
func NewCommitListBuilder(
|
||||||
|
log *logrus.Entry,
|
||||||
|
gitCommand *GitCommand,
|
||||||
|
osCommand *oscommands.OSCommand,
|
||||||
|
tr *i18n.TranslationSet,
|
||||||
|
) *CommitListBuilder {
|
||||||
return &CommitListBuilder{
|
return &CommitListBuilder{
|
||||||
Log: log,
|
Log: log,
|
||||||
GitCommand: gitCommand,
|
GitCommand: gitCommand,
|
||||||
@@ -72,10 +77,6 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
|||||||
|
|
||||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
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{
|
return &models.Commit{
|
||||||
Sha: sha,
|
Sha: sha,
|
||||||
Name: message,
|
Name: message,
|
||||||
@@ -83,7 +84,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
|||||||
ExtraInfo: extraInfo,
|
ExtraInfo: extraInfo,
|
||||||
UnixTimestamp: int64(unitTimestampInt),
|
UnixTimestamp: int64(unitTimestampInt),
|
||||||
Author: author,
|
Author: author,
|
||||||
IsMerge: isMerge,
|
Parents: strings.Split(parentHashes, " "),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +93,8 @@ type GetCommitsOptions struct {
|
|||||||
FilterPath string
|
FilterPath string
|
||||||
IncludeRebaseCommits bool
|
IncludeRebaseCommits bool
|
||||||
RefName string // e.g. "HEAD" or "my_branch"
|
RefName string // e.g. "HEAD" or "my_branch"
|
||||||
|
// determines if we show the whole git graph i.e. pass the '--all' flag
|
||||||
|
All bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||||
@@ -114,7 +117,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
|
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -153,7 +156,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
|||||||
cmd := c.getLogCmd(opts)
|
cmd := c.getLogCmd(opts)
|
||||||
|
|
||||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||||
if strings.Split(line, " ")[0] != "gpg:" {
|
if canExtractCommit(line) {
|
||||||
commit := c.extractCommitFromLine(line)
|
commit := c.extractCommitFromLine(line)
|
||||||
if commit.Sha == firstPushedCommit {
|
if commit.Sha == firstPushedCommit {
|
||||||
passedFirstPushedCommit = true
|
passedFirstPushedCommit = true
|
||||||
@@ -169,8 +172,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
|||||||
|
|
||||||
if rebaseMode != "" {
|
if rebaseMode != "" {
|
||||||
currentCommit := commits[len(rebasingCommits)]
|
currentCommit := commits[len(rebasingCommits)]
|
||||||
blue := color.New(color.FgYellow)
|
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
|
||||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +184,51 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
|||||||
return commits, nil
|
return commits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||||
|
commits, err := c.getRebasingCommits(rebaseMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commits) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commitShas := make([]string, len(commits))
|
||||||
|
for i, commit := range commits {
|
||||||
|
commitShas[i] = commit.Sha
|
||||||
|
}
|
||||||
|
|
||||||
|
// note that we're not filtering these as we do non-rebasing commits just because
|
||||||
|
// I suspect that will cause some damage
|
||||||
|
cmd := c.OSCommand.ExecutableFromString(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"git show %s --no-patch --oneline %s --abbrev=%d",
|
||||||
|
strings.Join(commitShas, " "),
|
||||||
|
prettyFormat,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||||
|
i := 0
|
||||||
|
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||||
|
if canExtractCommit(line) {
|
||||||
|
commit := c.extractCommitFromLine(line)
|
||||||
|
matchingCommit := commits[i]
|
||||||
|
commit.Action = matchingCommit.Action
|
||||||
|
commit.Status = matchingCommit.Status
|
||||||
|
hydratedCommits = append(hydratedCommits, commit)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return hydratedCommits, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||||
switch rebaseMode {
|
switch rebaseMode {
|
||||||
@@ -327,7 +374,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||||
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
|
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
|
||||||
return ignoringWarnings(output), nil
|
return ignoringWarnings(output), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +391,7 @@ func ignoringWarnings(commandOutput string) string {
|
|||||||
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
||||||
// all commits above this are deemed unpushed and marked as such.
|
// all commits above this are deemed unpushed and marked as such.
|
||||||
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
|
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
|
||||||
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
|
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -364,18 +411,37 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
|||||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := c.GitCommand.Config.GetUserConfig().Git.Log
|
||||||
|
|
||||||
|
orderFlag := "--" + config.Order
|
||||||
|
allFlag := ""
|
||||||
|
if opts.All {
|
||||||
|
allFlag = " --all"
|
||||||
|
}
|
||||||
|
|
||||||
return c.OSCommand.ExecutableFromString(
|
return c.OSCommand.ExecutableFromString(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
|
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
|
||||||
opts.RefName,
|
c.OSCommand.Quote(opts.RefName),
|
||||||
SEPARATION_CHAR,
|
orderFlag,
|
||||||
SEPARATION_CHAR,
|
allFlag,
|
||||||
SEPARATION_CHAR,
|
prettyFormat,
|
||||||
SEPARATION_CHAR,
|
|
||||||
SEPARATION_CHAR,
|
|
||||||
limitFlag,
|
limitFlag,
|
||||||
20,
|
20,
|
||||||
filterFlag,
|
filterFlag,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prettyFormat = fmt.Sprintf(
|
||||||
|
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
|
||||||
|
SEPARATION_CHAR,
|
||||||
|
SEPARATION_CHAR,
|
||||||
|
SEPARATION_CHAR,
|
||||||
|
SEPARATION_CHAR,
|
||||||
|
SEPARATION_CHAR,
|
||||||
|
)
|
||||||
|
|
||||||
|
func canExtractCommit(line string) bool {
|
||||||
|
return strings.Split(line, " ")[0] != "gpg:"
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func NewDummyCommitListBuilder() *CommitListBuilder {
|
|||||||
Log: utils.NewDummyLog(),
|
Log: utils.NewDummyLog(),
|
||||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||||
OSCommand: osCommand,
|
OSCommand: osCommand,
|
||||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,103 +15,102 @@ type GetStatusFileOptions struct {
|
|||||||
|
|
||||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||||
// check if config wants us ignoring untracked files
|
// check if config wants us ignoring untracked files
|
||||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
|
||||||
|
|
||||||
if untrackedFilesSetting == "" {
|
if untrackedFilesSetting == "" {
|
||||||
untrackedFilesSetting = "all"
|
untrackedFilesSetting = "all"
|
||||||
}
|
}
|
||||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
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 {
|
if err != nil {
|
||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
}
|
}
|
||||||
statusStrings := utils.SplitLines(statusOutput)
|
|
||||||
files := []*models.File{}
|
files := []*models.File{}
|
||||||
|
|
||||||
for _, statusString := range statusStrings {
|
for _, status := range statuses {
|
||||||
if strings.HasPrefix(statusString, "warning") {
|
if strings.HasPrefix(status.StatusString, "warning") {
|
||||||
c.Log.Warningf("warning when calling git status: %s", statusString)
|
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
change := statusString[0:2]
|
change := status.Change
|
||||||
stagedChange := change[0:1]
|
stagedChange := change[0:1]
|
||||||
unstagedChange := statusString[1:2]
|
unstagedChange := change[1:2]
|
||||||
filename := statusString[3:]
|
|
||||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||||
|
|
||||||
file := &models.File{
|
file := &models.File{
|
||||||
Name: filename,
|
Name: status.Name,
|
||||||
DisplayString: statusString,
|
PreviousName: status.PreviousName,
|
||||||
|
DisplayString: status.StatusString,
|
||||||
HasStagedChanges: !hasNoStagedChanges,
|
HasStagedChanges: !hasNoStagedChanges,
|
||||||
HasUnstagedChanges: unstagedChange != " ",
|
HasUnstagedChanges: unstagedChange != " ",
|
||||||
Tracked: !untracked,
|
Tracked: !untracked,
|
||||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||||
|
Added: unstagedChange == "A" || untracked,
|
||||||
HasMergeConflicts: hasMergeConflicts,
|
HasMergeConflicts: hasMergeConflicts,
|
||||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||||
Type: c.OSCommand.FileType(filename),
|
Type: c.OSCommand.FileType(status.Name),
|
||||||
ShortStatus: change,
|
ShortStatus: change,
|
||||||
}
|
}
|
||||||
files = append(files, file)
|
files = append(files, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitStatus returns the plaintext short status of the repo
|
// GitStatus returns the file status of the repo
|
||||||
type GitStatusOptions struct {
|
type GitStatusOptions struct {
|
||||||
NoRenames bool
|
NoRenames bool
|
||||||
UntrackedFilesArg string
|
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 := ""
|
noRenamesFlag := ""
|
||||||
if opts.NoRenames {
|
if opts.NoRenames {
|
||||||
noRenamesFlag = "--no-renames"
|
noRenamesFlag = "--no-renames"
|
||||||
}
|
}
|
||||||
|
|
||||||
statusLines, err := c.OSCommand.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return []FileStatus{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
statusLines = strings.Replace(statusLines, "\x00", "\n", -1)
|
splitLines := strings.Split(statusLines, "\x00")
|
||||||
return statusLines, nil
|
response := []FileStatus{}
|
||||||
}
|
|
||||||
|
|
||||||
// MergeStatusFiles merge status files
|
for i := 0; i < len(splitLines); i++ {
|
||||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
|
original := splitLines[i]
|
||||||
if len(oldFiles) == 0 {
|
|
||||||
return newFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
appendedIndexes := []int{}
|
if len(original) < 3 {
|
||||||
|
continue
|
||||||
// retain position of files we already could see
|
|
||||||
result := []*models.File{}
|
|
||||||
for _, oldFile := range oldFiles {
|
|
||||||
for newIndex, newFile := range newFiles {
|
|
||||||
if utils.IncludesInt(appendedIndexes, newIndex) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
|
|
||||||
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
|
|
||||||
|
|
||||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
|
||||||
result = append(result, newFile)
|
|
||||||
appendedIndexes = append(appendedIndexes, newIndex)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// append any new files to the end
|
status := FileStatus{
|
||||||
for index, newFile := range newFiles {
|
StatusString: original,
|
||||||
if !utils.IncludesInt(appendedIndexes, index) {
|
Change: original[:2],
|
||||||
result = append(result, newFile)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
@@ -13,26 +13,25 @@ import (
|
|||||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||||
commits := make([]*models.Commit, 0)
|
commits := make([]*models.Commit, 0)
|
||||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
|
||||||
|
|
||||||
filterPathArg := ""
|
filterPathArg := ""
|
||||||
if filterPath != "" {
|
if filterPath != "" {
|
||||||
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
|
||||||
onlyObtainedNewReflogCommits := false
|
onlyObtainedNewReflogCommits := false
|
||||||
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||||
match := re.FindStringSubmatch(line)
|
fields := strings.SplitN(line, " ", 3)
|
||||||
if len(match) <= 1 {
|
if len(fields) <= 2 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||||
|
|
||||||
commit := &models.Commit{
|
commit := &models.Commit{
|
||||||
Sha: match[1],
|
Sha: fields[0],
|
||||||
Name: match[3],
|
Name: fields[2],
|
||||||
UnixTimestamp: int64(unixTimestamp),
|
UnixTimestamp: int64(unixTimestamp),
|
||||||
Status: "reflog",
|
Status: "reflog",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
|
|||||||
return c.getUnfilteredStashEntries()
|
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 {
|
if err != nil {
|
||||||
return c.getUnfilteredStashEntries()
|
return c.getUnfilteredStashEntries()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"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) {
|
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||||
// get remote branches
|
// get remote branches, sorted by creation date (descending)
|
||||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -37,52 +25,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
|||||||
// first step is to get our remotes from go-git
|
// first step is to get our remotes from go-git
|
||||||
tags := make([]*models.Tag, len(split))
|
tags := make([]*models.Tag, len(split))
|
||||||
for i, tagName := range split {
|
for i, tagName := range split {
|
||||||
|
|
||||||
tags[i] = &models.Tag{
|
tags[i] = &models.Tag{
|
||||||
Name: tagName,
|
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
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,3 +24,26 @@ func (b *Branch) ID() string {
|
|||||||
func (b *Branch) Description() string {
|
func (b *Branch) Description() string {
|
||||||
return b.RefName()
|
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
|
Author string
|
||||||
UnixTimestamp int64
|
UnixTimestamp int64
|
||||||
|
|
||||||
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
|
// SHAs of parent commits (will be multiple if it's a merge commit)
|
||||||
IsMerge bool
|
Parents []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commit) ShortSha() string {
|
func (c *Commit) ShortSha() string {
|
||||||
@@ -35,3 +35,7 @@ func (c *Commit) ID() string {
|
|||||||
func (c *Commit) Description() string {
|
func (c *Commit) Description() string {
|
||||||
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
|
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
|
// CommitFile : A git commit file
|
||||||
type CommitFile struct {
|
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}'
|
// TODO: rename this to Path
|
||||||
Parent string
|
Name 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'
|
|
||||||
|
|
||||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
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
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,9 +8,11 @@ import (
|
|||||||
// duplicating this for now
|
// duplicating this for now
|
||||||
type File struct {
|
type File struct {
|
||||||
Name string
|
Name string
|
||||||
|
PreviousName string
|
||||||
HasStagedChanges bool
|
HasStagedChanges bool
|
||||||
HasUnstagedChanges bool
|
HasUnstagedChanges bool
|
||||||
Tracked bool
|
Tracked bool
|
||||||
|
Added bool
|
||||||
Deleted bool
|
Deleted bool
|
||||||
HasMergeConflicts bool
|
HasMergeConflicts bool
|
||||||
HasInlineMergeConflicts bool
|
HasInlineMergeConflicts bool
|
||||||
@@ -21,15 +21,25 @@ type File struct {
|
|||||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
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 {
|
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
|
// 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 {
|
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
|
// 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 {
|
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||||
for _, config := range configs {
|
for _, config := range configs {
|
||||||
if f.Name == config.Name {
|
if f.Name == config.Path {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package oscommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A command object is a general way to represent a command to be run on the
|
||||||
|
// command line. If you want to log the command you'll use .ToString() and
|
||||||
|
// if you want to run it you'll use .GetCmd()
|
||||||
|
type ICmdObj interface {
|
||||||
|
GetCmd() *exec.Cmd
|
||||||
|
ToString() string
|
||||||
|
AddEnvVars(...string) ICmdObj
|
||||||
|
}
|
||||||
|
|
||||||
|
type CmdObj struct {
|
||||||
|
cmdStr string
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||||
|
return self.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CmdObj) ToString() string {
|
||||||
|
return self.cmdStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||||
|
self.cmd.Env = append(self.cmd.Env, vars...)
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package oscommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/go-errors/errors"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||||
|
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||||
|
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||||
|
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
|
||||||
|
ttyText := ""
|
||||||
|
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, func(word string) string {
|
||||||
|
ttyText = ttyText + " " + word
|
||||||
|
|
||||||
|
prompts := map[string]string{
|
||||||
|
`.+'s password:`: "password",
|
||||||
|
`Password\s*for\s*'.+':`: "password",
|
||||||
|
`Username\s*for\s*'.+':`: "username",
|
||||||
|
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
|
||||||
|
}
|
||||||
|
|
||||||
|
for pattern, askFor := range prompts {
|
||||||
|
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||||
|
ttyText = ""
|
||||||
|
return promptUserForCredential(askFor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
return errMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
|
||||||
|
// separate for windows and other OS's
|
||||||
|
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
|
||||||
|
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmdHandler struct {
|
||||||
|
stdoutPipe io.Reader
|
||||||
|
stdinPipe io.Writer
|
||||||
|
close func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
|
||||||
|
// Output is a function that executes by every word that gets read by bufio
|
||||||
|
// As return of output you need to give a string that will be written to stdin
|
||||||
|
// NOTE: If the return data is empty it won't write anything to stdin
|
||||||
|
func RunCommandWithOutputLiveAux(
|
||||||
|
c *OSCommand,
|
||||||
|
cmdObj ICmdObj,
|
||||||
|
writer io.Writer,
|
||||||
|
// handleOutput takes a word from stdout and returns a string to be written to stdin.
|
||||||
|
// See DetectUnamePass above for how this is used to check for a username/password request
|
||||||
|
handleOutput func(string) string,
|
||||||
|
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
|
||||||
|
) error {
|
||||||
|
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||||
|
c.LogCommand(cmdObj.ToString(), true)
|
||||||
|
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
|
||||||
|
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = io.MultiWriter(writer, &stderr)
|
||||||
|
|
||||||
|
handler, err := startCmd(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if closeErr := handler.close(); closeErr != nil {
|
||||||
|
c.Log.Error(closeErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tr := io.TeeReader(handler.stdoutPipe, writer)
|
||||||
|
|
||||||
|
go utils.Safe(func() {
|
||||||
|
scanner := bufio.NewScanner(tr)
|
||||||
|
scanner.Split(scanWordsWithNewLines)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
output := strings.Trim(text, " ")
|
||||||
|
toInput := handleOutput(output)
|
||||||
|
if toInput != "" {
|
||||||
|
_, _ = handler.stdinPipe.Write([]byte(toInput))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||||
|
// For specific comments about this function take a look at: bufio.ScanWords
|
||||||
|
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
start := 0
|
||||||
|
for width := 0; start < len(data); start += width {
|
||||||
|
var r rune
|
||||||
|
r, width = utf8.DecodeRune(data[start:])
|
||||||
|
if !isSpace(r) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for width, i := 0, start; i < len(data); i += width {
|
||||||
|
var r rune
|
||||||
|
r, width = utf8.DecodeRune(data[i:])
|
||||||
|
if isSpace(r) {
|
||||||
|
return i + width, data[start:i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if atEOF && len(data) > start {
|
||||||
|
return len(data), data[start:], nil
|
||||||
|
}
|
||||||
|
return start, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||||
|
// For specific comments about this function take a look at: bufio.isSpace
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
if r <= '\u00FF' {
|
||||||
|
switch r {
|
||||||
|
case ' ', '\t', '\v', '\f':
|
||||||
|
return true
|
||||||
|
case '\u0085', '\u00A0':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if '\u2000' <= r && r <= '\u200a' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -1,97 +1,37 @@
|
|||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package oscommands
|
package oscommands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"io"
|
||||||
"bytes"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
func RunCommandWithOutputLiveWrapper(
|
||||||
// Output is a function that executes by every word that gets read by bufio
|
c *OSCommand,
|
||||||
// As return of output you need to give a string that will be written to stdin
|
cmdObj ICmdObj,
|
||||||
// NOTE: If the return data is empty it won't written anything to stdin
|
writer io.Writer,
|
||||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
output func(string) string,
|
||||||
c.Log.WithField("command", command).Info("RunCommand")
|
) error {
|
||||||
cmd := c.ExecutableFromString(command)
|
return RunCommandWithOutputLiveAux(
|
||||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
c,
|
||||||
|
cmdObj,
|
||||||
|
writer,
|
||||||
|
output,
|
||||||
|
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||||
|
ptmx, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
return &cmdHandler{
|
||||||
cmd.Stderr = &stderr
|
stdoutPipe: ptmx,
|
||||||
|
stdinPipe: ptmx,
|
||||||
ptmx, err := pty.Start(cmd)
|
close: ptmx.Close,
|
||||||
|
}, nil
|
||||||
if err != nil {
|
},
|
||||||
return err
|
)
|
||||||
}
|
|
||||||
|
|
||||||
go utils.Safe(func() {
|
|
||||||
scanner := bufio.NewScanner(ptmx)
|
|
||||||
scanner.Split(scanWordsWithNewLines)
|
|
||||||
for scanner.Scan() {
|
|
||||||
toOutput := strings.Trim(scanner.Text(), " ")
|
|
||||||
_, _ = ptmx.WriteString(output(toOutput))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
ptmx.Close()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
|
||||||
// For specific comments about this function take a look at: bufio.ScanWords
|
|
||||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
||||||
start := 0
|
|
||||||
for width := 0; start < len(data); start += width {
|
|
||||||
var r rune
|
|
||||||
r, width = utf8.DecodeRune(data[start:])
|
|
||||||
if !isSpace(r) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for width, i := 0, start; i < len(data); i += width {
|
|
||||||
var r rune
|
|
||||||
r, width = utf8.DecodeRune(data[i:])
|
|
||||||
if isSpace(r) {
|
|
||||||
return i + width, data[start:i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if atEOF && len(data) > start {
|
|
||||||
return len(data), data[start:], nil
|
|
||||||
}
|
|
||||||
return start, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
|
||||||
// For specific comments about this function take a look at: bufio.isSpace
|
|
||||||
func isSpace(r rune) bool {
|
|
||||||
if r <= '\u00FF' {
|
|
||||||
switch r {
|
|
||||||
case ' ', '\t', '\v', '\f':
|
|
||||||
return true
|
|
||||||
case '\u0085', '\u00A0':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if '\u2000' <= r && r <= '\u200a' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
switch r {
|
|
||||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,63 @@
|
|||||||
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package oscommands
|
package oscommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
b bytes.Buffer
|
||||||
|
m sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||||
|
b.m.Lock()
|
||||||
|
defer b.m.Unlock()
|
||||||
|
return b.b.Read(p)
|
||||||
|
}
|
||||||
|
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||||
|
b.m.Lock()
|
||||||
|
defer b.m.Unlock()
|
||||||
|
return b.b.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
|
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
func RunCommandWithOutputLiveWrapper(
|
||||||
return c.RunCommand(command)
|
c *OSCommand,
|
||||||
|
cmdObj ICmdObj,
|
||||||
|
writer io.Writer,
|
||||||
|
output func(string) string,
|
||||||
|
) error {
|
||||||
|
return RunCommandWithOutputLiveAux(
|
||||||
|
c,
|
||||||
|
cmdObj,
|
||||||
|
writer,
|
||||||
|
output,
|
||||||
|
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||||
|
stdoutReader, stdoutWriter := io.Pipe()
|
||||||
|
cmd.Stdout = stdoutWriter
|
||||||
|
|
||||||
|
buf := &Buffer{}
|
||||||
|
cmd.Stdin = buf
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// because we don't yet have windows support for a pty, we instead just
|
||||||
|
// pass our standard stream handlers and because there's no pty to close
|
||||||
|
// we pass a no-op function for that.
|
||||||
|
return &cmdHandler{
|
||||||
|
stdoutPipe: stdoutReader,
|
||||||
|
stdinPipe: buf,
|
||||||
|
close: func() error { return nil },
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-67
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -24,10 +23,8 @@ import (
|
|||||||
// Platform stores the os state
|
// Platform stores the os state
|
||||||
type Platform struct {
|
type Platform struct {
|
||||||
OS string
|
OS string
|
||||||
CatCmd string
|
|
||||||
Shell string
|
Shell string
|
||||||
ShellArg string
|
ShellArg string
|
||||||
EscapedQuote string
|
|
||||||
OpenCommand string
|
OpenCommand string
|
||||||
OpenLinkCommand string
|
OpenLinkCommand string
|
||||||
}
|
}
|
||||||
@@ -40,6 +37,44 @@ type OSCommand struct {
|
|||||||
Command func(string, ...string) *exec.Cmd
|
Command func(string, ...string) *exec.Cmd
|
||||||
BeforeExecuteCmd func(*exec.Cmd)
|
BeforeExecuteCmd func(*exec.Cmd)
|
||||||
Getenv func(string) string
|
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
|
// NewOSCommand os command runner
|
||||||
@@ -51,15 +86,51 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
|||||||
Command: secureexec.Command,
|
Command: secureexec.Command,
|
||||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||||
Getenv: os.Getenv,
|
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.
|
// SetCommand sets the command function used by the struct.
|
||||||
// To be used for testing only
|
// To be used for testing only
|
||||||
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||||
c.Command = 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)) {
|
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||||
c.BeforeExecuteCmd = cmd
|
c.BeforeExecuteCmd = cmd
|
||||||
}
|
}
|
||||||
@@ -69,9 +140,12 @@ type RunCommandOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
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 := 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...)
|
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||||
|
|
||||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,17 +165,18 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
|||||||
if formatArgs != nil {
|
if formatArgs != nil {
|
||||||
command = fmt.Sprintf(formatString, formatArgs...)
|
command = fmt.Sprintf(formatString, formatArgs...)
|
||||||
}
|
}
|
||||||
c.Log.WithField("command", command).Info("RunCommand")
|
|
||||||
cmd := c.ExecutableFromString(command)
|
cmd := c.ExecutableFromString(command)
|
||||||
|
c.LogExecCmd(cmd)
|
||||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log.WithField("command", command).Error(err)
|
c.Log.WithField("command", command).Error(output)
|
||||||
}
|
}
|
||||||
return output, err
|
return output, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunExecutableWithOutput runs an executable file and returns its output
|
// RunExecutableWithOutput runs an executable file and returns its output
|
||||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||||
|
c.LogExecCmd(cmd)
|
||||||
c.BeforeExecuteCmd(cmd)
|
c.BeforeExecuteCmd(cmd)
|
||||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||||
}
|
}
|
||||||
@@ -125,7 +200,14 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
|||||||
quotedCommand := ""
|
quotedCommand := ""
|
||||||
// Windows does not seem to like quotes around the command
|
// Windows does not seem to like quotes around the command
|
||||||
if c.Platform.OS == "windows" {
|
if c.Platform.OS == "windows" {
|
||||||
quotedCommand = commandStr
|
quotedCommand = strings.NewReplacer(
|
||||||
|
"^", "^^",
|
||||||
|
"&", "^&",
|
||||||
|
"|", "^|",
|
||||||
|
"<", "^<",
|
||||||
|
">", "^>",
|
||||||
|
"%", "^%",
|
||||||
|
).Replace(commandStr)
|
||||||
} else {
|
} else {
|
||||||
quotedCommand = c.Quote(commandStr)
|
quotedCommand = c.Quote(commandStr)
|
||||||
}
|
}
|
||||||
@@ -134,44 +216,23 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
|||||||
return c.ExecutableFromString(shellCommand)
|
return c.ExecutableFromString(shellCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
|
||||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
|
||||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
|
||||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
|
||||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
|
||||||
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
|
|
||||||
ttyText := ""
|
|
||||||
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
|
|
||||||
ttyText = ttyText + " " + word
|
|
||||||
|
|
||||||
prompts := map[string]string{
|
|
||||||
`.+'s password:`: "password",
|
|
||||||
`Password\s*for\s*'.+':`: "password",
|
|
||||||
`Username\s*for\s*'.+':`: "username",
|
|
||||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
|
|
||||||
}
|
|
||||||
|
|
||||||
for pattern, askFor := range prompts {
|
|
||||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
|
||||||
ttyText = ""
|
|
||||||
return promptUserForCredential(askFor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
})
|
|
||||||
return errMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// RunCommand runs a command and just returns the error
|
// RunCommand runs a command and just returns the error
|
||||||
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||||
return err
|
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.ShellCommandFromString(command)
|
||||||
|
c.LogExecCmd(cmd)
|
||||||
|
|
||||||
|
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// FileType tells us if the file is a file, directory or other
|
// FileType tells us if the file is a file, directory or other
|
||||||
func (c *OSCommand) FileType(path string) string {
|
func (c *OSCommand) FileType(path string) string {
|
||||||
fileInfo, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
@@ -184,16 +245,6 @@ func (c *OSCommand) FileType(path string) string {
|
|||||||
return "file"
|
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) {
|
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||||
outputString := string(output)
|
outputString := string(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,21 +264,21 @@ func (c *OSCommand) OpenFile(filename string) error {
|
|||||||
templateValues := map[string]string{
|
templateValues := map[string]string{
|
||||||
"filename": c.Quote(filename),
|
"filename": c.Quote(filename),
|
||||||
}
|
}
|
||||||
|
|
||||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||||
err := c.RunCommand(command)
|
err := c.RunShellCommand(command)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenLink opens a file with the given
|
// OpenLink opens a file with the given
|
||||||
func (c *OSCommand) OpenLink(link string) error {
|
func (c *OSCommand) OpenLink(link string) error {
|
||||||
|
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
|
||||||
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
|
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
|
||||||
templateValues := map[string]string{
|
templateValues := map[string]string{
|
||||||
"link": c.Quote(link),
|
"link": c.Quote(link),
|
||||||
}
|
}
|
||||||
|
|
||||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||||
err := c.RunCommand(command)
|
err := c.RunShellCommand(command)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,26 +289,39 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
|||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||||
}
|
}
|
||||||
|
c.LogExecCmd(cmd)
|
||||||
return 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
|
// Quote wraps a message in platform-specific quotation marks
|
||||||
func (c *OSCommand) Quote(message string) string {
|
func (c *OSCommand) Quote(message string) string {
|
||||||
|
var quote string
|
||||||
if c.Platform.OS == "windows" {
|
if c.Platform.OS == "windows" {
|
||||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
quote = `\"`
|
||||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
message = strings.NewReplacer(
|
||||||
|
`"`, `"'"'"`,
|
||||||
|
`\"`, `\\"`,
|
||||||
|
).Replace(message)
|
||||||
} else {
|
} else {
|
||||||
message = strings.Replace(message, `\`, `\\`, -1)
|
quote = `"`
|
||||||
message = strings.Replace(message, `"`, `\"`, -1)
|
message = strings.NewReplacer(
|
||||||
message = strings.Replace(message, "`", "\\`", -1)
|
`\`, `\\`,
|
||||||
message = strings.Replace(message, "$", "\\$", -1)
|
`"`, `\"`,
|
||||||
|
`$`, `\$`,
|
||||||
|
"`", "\\`",
|
||||||
|
).Replace(message)
|
||||||
}
|
}
|
||||||
escapedQuote := c.Platform.EscapedQuote
|
return quote + message + quote
|
||||||
return escapedQuote + message + escapedQuote
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppendLineToFile adds a new line in file
|
// AppendLineToFile adds a new line in file
|
||||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
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)
|
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.WrapError(err)
|
return utils.WrapError(err)
|
||||||
@@ -278,6 +342,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
|||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
return "", utils.WrapError(err)
|
return "", utils.WrapError(err)
|
||||||
}
|
}
|
||||||
|
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
|
||||||
|
|
||||||
if _, err := tmpfile.WriteString(content); err != nil {
|
if _, err := tmpfile.WriteString(content); err != nil {
|
||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
@@ -293,6 +358,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
|||||||
|
|
||||||
// CreateFileWithContent creates a file with the given content
|
// CreateFileWithContent creates a file with the given content
|
||||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
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 {
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
c.Log.Error(err)
|
c.Log.Error(err)
|
||||||
return err
|
return err
|
||||||
@@ -308,6 +374,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
|||||||
|
|
||||||
// Remove removes a file or directory at the specified path
|
// Remove removes a file or directory at the specified path
|
||||||
func (c *OSCommand) Remove(filename string) error {
|
func (c *OSCommand) Remove(filename string) error {
|
||||||
|
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
|
||||||
err := os.RemoveAll(filename)
|
err := os.RemoveAll(filename)
|
||||||
return utils.WrapError(err)
|
return utils.WrapError(err)
|
||||||
}
|
}
|
||||||
@@ -328,6 +395,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
|||||||
// before running it
|
// before running it
|
||||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||||
c.BeforeExecuteCmd(cmd)
|
c.BeforeExecuteCmd(cmd)
|
||||||
|
c.LogExecCmd(cmd)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
outString := string(out)
|
outString := string(out)
|
||||||
c.Log.Info(outString)
|
c.Log.Info(outString)
|
||||||
@@ -349,19 +417,18 @@ func (c *OSCommand) GetLazygitPath() string {
|
|||||||
return `"` + filepath.ToSlash(ex) + `"`
|
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
|
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||||
|
|
||||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||||
|
logCmdStr := ""
|
||||||
for i, str := range commandStrings {
|
for i, str := range commandStrings {
|
||||||
|
if i > 0 {
|
||||||
|
logCmdStr += " | "
|
||||||
|
}
|
||||||
|
logCmdStr += str
|
||||||
cmds[i] = c.ExecutableFromString(str)
|
cmds[i] = c.ExecutableFromString(str)
|
||||||
}
|
}
|
||||||
|
c.LogCommand(logCmdStr, true)
|
||||||
|
|
||||||
for i := 0; i < len(cmds)-1; i++ {
|
for i := 0; i < len(cmds)-1; i++ {
|
||||||
stdout, err := cmds[i].StdoutPipe()
|
stdout, err := cmds[i].StdoutPipe()
|
||||||
@@ -452,5 +519,41 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||||
|
escaped := strings.Replace(str, "\n", "\\n", -1)
|
||||||
|
truncated := utils.TruncateWithEllipsis(escaped, 40)
|
||||||
|
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", truncated), false)
|
||||||
return clipboard.WriteAll(str)
|
return clipboard.WriteAll(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) RemoveFile(path string) error {
|
||||||
|
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
|
||||||
|
|
||||||
|
return c.removeFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
|
||||||
|
args := str.ToArgv(cmdStr)
|
||||||
|
cmd := c.Command(args[0], args[1:]...)
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
|
||||||
|
return &CmdObj{
|
||||||
|
cmdStr: cmdStr,
|
||||||
|
cmd: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
|
||||||
|
cmd := c.Command(args[0], args[1:]...)
|
||||||
|
|
||||||
|
return &CmdObj{
|
||||||
|
cmdStr: strings.Join(args, " "),
|
||||||
|
cmd: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
|
||||||
|
return &CmdObj{
|
||||||
|
cmdStr: strings.Join(cmd.Args, " "),
|
||||||
|
cmd: cmd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package oscommands
|
package oscommands
|
||||||
@@ -9,10 +10,8 @@ import (
|
|||||||
func getPlatform() *Platform {
|
func getPlatform() *Platform {
|
||||||
return &Platform{
|
return &Platform{
|
||||||
OS: runtime.GOOS,
|
OS: runtime.GOOS,
|
||||||
CatCmd: "cat",
|
|
||||||
Shell: "bash",
|
Shell: "bash",
|
||||||
ShellArg: "-c",
|
ShellArg: "-c",
|
||||||
EscapedQuote: `"`,
|
|
||||||
OpenCommand: "open {{filename}}",
|
OpenCommand: "open {{filename}}",
|
||||||
OpenLinkCommand: "open {{link}}",
|
OpenLinkCommand: "open {{link}}",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package oscommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOSCommandOpenFileDarwin is a function.
|
||||||
|
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
filename string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `open "test"`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename with spaces",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
OSCmd := NewDummyOSCommand()
|
||||||
|
OSCmd.Platform.OS = "darwin"
|
||||||
|
OSCmd.Command = s.command
|
||||||
|
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||||
|
|
||||||
|
s.test(OSCmd.OpenFile(s.filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOSCommandOpenFileLinux tests the OpenFile command on Linux
|
||||||
|
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
filename string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename with spaces",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"let's_test_with_single_quote",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$USER.txt",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "bash", name)
|
||||||
|
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
OSCmd := NewDummyOSCommand()
|
||||||
|
OSCmd.Command = s.command
|
||||||
|
OSCmd.Platform.OS = "linux"
|
||||||
|
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||||
|
|
||||||
|
s.test(OSCmd.OpenFile(s.filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,8 @@ package oscommands
|
|||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,57 +57,6 @@ func TestOSCommandRunCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOSCommandOpenFile is a function.
|
|
||||||
func TestOSCommandOpenFile(t *testing.T) {
|
|
||||||
type scenario struct {
|
|
||||||
filename string
|
|
||||||
command func(string, ...string) *exec.Cmd
|
|
||||||
test func(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
scenarios := []scenario{
|
|
||||||
{
|
|
||||||
"test",
|
|
||||||
func(name string, arg ...string) *exec.Cmd {
|
|
||||||
return secureexec.Command("exit", "1")
|
|
||||||
},
|
|
||||||
func(err error) {
|
|
||||||
assert.Error(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test",
|
|
||||||
func(name string, arg ...string) *exec.Cmd {
|
|
||||||
assert.Equal(t, "open", name)
|
|
||||||
assert.Equal(t, []string{"test"}, arg)
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename with spaces",
|
|
||||||
func(name string, arg ...string) *exec.Cmd {
|
|
||||||
assert.Equal(t, "open", name)
|
|
||||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range scenarios {
|
|
||||||
OSCmd := NewDummyOSCommand()
|
|
||||||
OSCmd.Command = s.command
|
|
||||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
|
||||||
|
|
||||||
s.test(OSCmd.OpenFile(s.filename))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestOSCommandQuote is a function.
|
// TestOSCommandQuote is a function.
|
||||||
func TestOSCommandQuote(t *testing.T) {
|
func TestOSCommandQuote(t *testing.T) {
|
||||||
osCommand := NewDummyOSCommand()
|
osCommand := NewDummyOSCommand()
|
||||||
@@ -118,7 +65,7 @@ func TestOSCommandQuote(t *testing.T) {
|
|||||||
|
|
||||||
actual := osCommand.Quote("hello `test`")
|
actual := osCommand.Quote("hello `test`")
|
||||||
|
|
||||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
expected := "\"hello \\`test\\`\""
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
@@ -131,7 +78,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
|||||||
|
|
||||||
actual := osCommand.Quote("hello 'test'")
|
actual := osCommand.Quote("hello 'test'")
|
||||||
|
|
||||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
expected := `"hello 'test'"`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
@@ -144,7 +91,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
|||||||
|
|
||||||
actual := osCommand.Quote(`hello "test"`)
|
actual := osCommand.Quote(`hello "test"`)
|
||||||
|
|
||||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
expected := `"hello \"test\""`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
@@ -155,9 +102,9 @@ func TestOSCommandQuoteWindows(t *testing.T) {
|
|||||||
|
|
||||||
osCommand.Platform.OS = "windows"
|
osCommand.Platform.OS = "windows"
|
||||||
|
|
||||||
actual := osCommand.Quote(`hello "test"`)
|
actual := osCommand.Quote(`hello "test" 'test2'`)
|
||||||
|
|
||||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
expected := `\"hello "'"'"test"'"'" 'test2'\"`
|
||||||
|
|
||||||
assert.EqualValues(t, expected, actual)
|
assert.EqualValues(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package oscommands
|
|||||||
|
|
||||||
func getPlatform() *Platform {
|
func getPlatform() *Platform {
|
||||||
return &Platform{
|
return &Platform{
|
||||||
OS: "windows",
|
OS: "windows",
|
||||||
CatCmd: "cmd /c type",
|
Shell: "cmd",
|
||||||
Shell: "cmd",
|
ShellArg: "/c",
|
||||||
ShellArg: "/c",
|
|
||||||
EscapedQuote: `\"`,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package oscommands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
|
||||||
|
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
filename string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("exit", "1")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "cmd", name)
|
||||||
|
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename with spaces",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "cmd", name)
|
||||||
|
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"let's_test_with_single_quote",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "cmd", name)
|
||||||
|
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$USER.txt",
|
||||||
|
func(name string, arg ...string) *exec.Cmd {
|
||||||
|
assert.Equal(t, "cmd", name)
|
||||||
|
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
OSCmd := NewDummyOSCommand()
|
||||||
|
OSCmd.Command = s.command
|
||||||
|
OSCmd.Platform.OS = "windows"
|
||||||
|
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
|
||||||
|
|
||||||
|
s.test(OSCmd.OpenFile(s.filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package patch
|
package patch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -9,9 +8,11 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PatchStatus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// UNSELECTED is for when the commit file has not been added to the patch in any way
|
// 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,
|
// WHOLE is for when you want to add the whole diff of a file to the patch,
|
||||||
// including e.g. if it was deleted
|
// including e.g. if it was deleted
|
||||||
WHOLE
|
WHOLE
|
||||||
@@ -20,7 +21,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fileInfo struct {
|
type fileInfo struct {
|
||||||
mode int // one of WHOLE/PART
|
mode PatchStatus
|
||||||
includedLineIndices []int
|
includedLineIndices []int
|
||||||
diff string
|
diff string
|
||||||
}
|
}
|
||||||
@@ -81,20 +82,25 @@ func (p *PatchManager) removeFile(info *fileInfo) {
|
|||||||
info.includedLineIndices = nil
|
info.includedLineIndices = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PatchManager) ToggleFileWhole(filename string) error {
|
func (p *PatchManager) AddFileWhole(filename string) error {
|
||||||
info, err := p.getFileInfo(filename)
|
info, err := p.getFileInfo(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
switch info.mode {
|
|
||||||
case UNSELECTED, PART:
|
p.addFileWhole(info)
|
||||||
p.addFileWhole(info)
|
|
||||||
case WHOLE:
|
return nil
|
||||||
p.removeFile(info)
|
}
|
||||||
default:
|
|
||||||
return errors.New("unknown file mode")
|
func (p *PatchManager) RemoveFile(filename string) error {
|
||||||
|
info, err := p.getFileInfo(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.removeFile(info)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +182,8 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
|||||||
if plain {
|
if plain {
|
||||||
return patch
|
return patch
|
||||||
}
|
}
|
||||||
parser, err := NewPatchParser(p.Log, patch)
|
parser := NewPatchParser(p.Log, patch)
|
||||||
if err != nil {
|
|
||||||
// swallowing for now
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// not passing included lines because we don't want to see them in the secondary panel
|
// not passing included lines because we don't want to see them in the secondary panel
|
||||||
return parser.Render(-1, -1, nil)
|
return parser.Render(-1, -1, nil)
|
||||||
}
|
}
|
||||||
@@ -216,7 +219,11 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
|||||||
return result
|
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]
|
info, ok := p.fileInfoMap[filename]
|
||||||
if !ok {
|
if !ok {
|
||||||
return UNSELECTED
|
return UNSELECTED
|
||||||
|
|||||||
@@ -138,7 +138,14 @@ func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string,
|
|||||||
|
|
||||||
// I want to know, given a hunk, what line a given index is on
|
// I want to know, given a hunk, what line a given index is on
|
||||||
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
||||||
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
|
n := idx - hunk.FirstLineIdx - 1
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
} else if n >= len(hunk.bodyLines) {
|
||||||
|
n = len(hunk.bodyLines) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := hunk.bodyLines[0:n]
|
||||||
|
|
||||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PatchLineKind int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PATCH_HEADER = iota
|
PATCH_HEADER PatchLineKind = iota
|
||||||
COMMIT_SHA
|
COMMIT_SHA
|
||||||
COMMIT_DESCRIPTION
|
COMMIT_DESCRIPTION
|
||||||
HUNK_HEADER
|
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.
|
// 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 {
|
type PatchLine struct {
|
||||||
Kind int
|
Kind PatchLineKind
|
||||||
Content string // something like '+ hello' (note the first character is not removed)
|
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
|
// NewPatchParser builds a new branch list builder
|
||||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
|
||||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
hunkStarts, stageableLines, patchLines := parsePatch(patch)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
patchHunks := GetHunksFromDiff(patch)
|
patchHunks := GetHunksFromDiff(patch)
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
|||||||
StageableLines: stageableLines,
|
StageableLines: stageableLines,
|
||||||
PatchLines: patchLines,
|
PatchLines: patchLines,
|
||||||
PatchHunks: patchHunks,
|
PatchHunks: patchHunks,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
// 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 {
|
if l.Kind == HUNK_HEADER {
|
||||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||||
match := re.FindStringSubmatch(content)
|
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 {
|
switch l.Kind {
|
||||||
case PATCH_HEADER:
|
case PATCH_HEADER:
|
||||||
colorAttr = color.Bold
|
textStyle = textStyle.SetBold()
|
||||||
case ADDITION:
|
case ADDITION:
|
||||||
colorAttr = color.FgGreen
|
textStyle = style.FgGreen
|
||||||
case DELETION:
|
case DELETION:
|
||||||
colorAttr = color.FgRed
|
textStyle = style.FgRed
|
||||||
case COMMIT_SHA:
|
case COMMIT_SHA:
|
||||||
colorAttr = color.FgYellow
|
textStyle = style.FgYellow
|
||||||
default:
|
|
||||||
colorAttr = theme.DefaultTextColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return coloredString(colorAttr, content, selected, included)
|
return coloredString(textStyle, content, selected, included)
|
||||||
}
|
}
|
||||||
|
|
||||||
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
|
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
|
||||||
var cl *color.Color
|
|
||||||
attributes := []color.Attribute{colorAttr}
|
|
||||||
if selected {
|
if selected {
|
||||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
|
||||||
}
|
}
|
||||||
cl = color.New(attributes...)
|
|
||||||
var clIncluded *color.Color
|
firstCharStyle := textStyle
|
||||||
if included {
|
if included {
|
||||||
clIncluded = color.New(append(attributes, color.BgGreen)...)
|
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
|
||||||
} else {
|
|
||||||
clIncluded = color.New(attributes...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(str) < 2 {
|
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")
|
lines := strings.Split(patch, "\n")
|
||||||
hunkStarts := []int{}
|
hunkStarts := []int{}
|
||||||
stageableLines := []int{}
|
stageableLines := []int{}
|
||||||
pastFirstHunkHeader := false
|
pastFirstHunkHeader := false
|
||||||
pastCommitDescription := true
|
pastCommitDescription := true
|
||||||
patchLines := make([]*PatchLine, len(lines))
|
patchLines := make([]*PatchLine, len(lines))
|
||||||
var lineKind int
|
var lineKind PatchLineKind
|
||||||
var firstChar string
|
var firstChar string
|
||||||
for index, line := range lines {
|
for index, line := range lines {
|
||||||
firstChar = " "
|
firstChar = " "
|
||||||
@@ -183,7 +176,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
|||||||
}
|
}
|
||||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
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
|
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||||
@@ -201,6 +194,19 @@ func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndic
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PlainRenderLines returns the non-coloured string of diff part from firstLineIndex to
|
||||||
|
// lastLineIndex
|
||||||
|
func (p *PatchParser) PlainRenderLines(firstLineIndex, lastLineIndex int) string {
|
||||||
|
linesToCopy := p.PatchLines[firstLineIndex : lastLineIndex+1]
|
||||||
|
|
||||||
|
renderedLines := make([]string, len(linesToCopy))
|
||||||
|
for index, line := range linesToCopy {
|
||||||
|
renderedLines[index] = line.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(renderedLines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||||
// note this will actually include the current index if it is stageable
|
// note this will actually include the current index if it is stageable
|
||||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd
|
|||||||
}
|
}
|
||||||
|
|
||||||
// time to amend the selected commit
|
// time to amend the selected commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the destination commit
|
// amend the destination commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
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:
|
// 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
|
// one where we handle the possibility of a credential request, and the other
|
||||||
// where we continue the rebase
|
// where we continue the rebase
|
||||||
if c.usingGpg() {
|
if c.UsingGpg() {
|
||||||
return errors.New(c.Tr.DisabledForGPG)
|
return errors.New(c.Tr.DisabledForGPG)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the source commit
|
// amend the source commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the destination commit
|
// amend the destination commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
|||||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
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 stash {
|
||||||
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -158,7 +158,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the commit
|
// amend the commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amend the commit
|
// amend the commit
|
||||||
if _, err := c.AmendHead(); err != nil {
|
if err := c.AmendHead(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
|||||||
|
|
||||||
head_message, _ := c.GetHeadCommitMessage()
|
head_message, _ := c.GetHeadCommitMessage()
|
||||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,62 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Name string
|
Name string
|
||||||
PullRequestURL 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
|
// PullRequest opens a link in browser to create new pull request
|
||||||
@@ -28,31 +76,6 @@ type RepoInformation struct {
|
|||||||
Repository string
|
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 {
|
func getServices(config config.AppConfigurer) []*Service {
|
||||||
services := []*Service{
|
services := []*Service{
|
||||||
NewService("github", "github.com", "github.com"),
|
NewService("github", "github.com", "github.com"),
|
||||||
@@ -90,27 +113,27 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create opens link to new pull request in browser
|
// Create opens link to new pull request in browser
|
||||||
func (pr *PullRequest) Create(branch *models.Branch) error {
|
func (pr *PullRequest) Create(from string, to string) (string, error) {
|
||||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||||
if err != nil {
|
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
|
// CopyURL copies the pull request URL to the clipboard
|
||||||
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
|
func (pr *PullRequest) CopyURL(from string, to string) (string, error) {
|
||||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||||
if err != nil {
|
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) {
|
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
|
||||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
|
||||||
|
|
||||||
if !branchExistsOnRemote {
|
if !branchExistsOnRemote {
|
||||||
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||||
@@ -131,9 +154,8 @@ func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
repoInfo := getRepoInfoFromURL(repoURL)
|
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
|
return pullRequestURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCreatePullRequest is a function.
|
||||||
|
func TestCreatePullRequest(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
from string
|
||||||
|
to string
|
||||||
|
remoteUrl string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(url string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket",
|
||||||
|
from: "feature/profile-page",
|
||||||
|
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||||
|
from: "feature/events",
|
||||||
|
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on github",
|
||||||
|
from: "feature/sum-operation",
|
||||||
|
remoteUrl: "git@github.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||||
|
from: "feature/profile-page/avatar",
|
||||||
|
to: "feature/profile-page",
|
||||||
|
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||||
|
from: "feature/remote-events",
|
||||||
|
to: "feature/events",
|
||||||
|
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on github with specific target branch",
|
||||||
|
from: "feature/sum-operation",
|
||||||
|
to: "feature/operations",
|
||||||
|
remoteUrl: "git@github.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab",
|
||||||
|
from: "feature/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||||
|
from: "feature/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||||
|
from: "feature/commit-ui",
|
||||||
|
to: "epic/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||||
|
from: "feature/commit-ui",
|
||||||
|
to: "epic/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "bash")
|
||||||
|
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Throws an error if git service is unsupported",
|
||||||
|
from: "feature/divide-operation",
|
||||||
|
remoteUrl: "git@something.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
gitCommand := NewDummyGitCommand()
|
||||||
|
gitCommand.OSCommand.Command = s.command
|
||||||
|
gitCommand.OSCommand.Platform.OS = "darwin"
|
||||||
|
gitCommand.OSCommand.Platform.Shell = "bash"
|
||||||
|
gitCommand.OSCommand.Platform.ShellArg = "-c"
|
||||||
|
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
||||||
|
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||||
|
// valid configuration for a custom service URL
|
||||||
|
"git.work.com": "gitlab:code.work.com",
|
||||||
|
// invalid configurations for a custom service URL
|
||||||
|
"invalid.work.com": "noservice:invalid.work.com",
|
||||||
|
"noservice.work.com": "noservice.work.com",
|
||||||
|
}
|
||||||
|
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||||
|
dummyPullRequest := NewPullRequest(gitCommand)
|
||||||
|
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,131 +39,3 @@ func TestGetRepoInfoFromURL(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCreatePullRequest is a function.
|
|
||||||
func TestCreatePullRequest(t *testing.T) {
|
|
||||||
type scenario struct {
|
|
||||||
testName string
|
|
||||||
branch *models.Branch
|
|
||||||
remoteUrl string
|
|
||||||
command func(string, ...string) *exec.Cmd
|
|
||||||
test func(err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
scenarios := []scenario{
|
|
||||||
{
|
|
||||||
testName: "Opens a link to new pull request on bitbucket",
|
|
||||||
branch: &models.Branch{
|
|
||||||
Name: "feature/profile-page",
|
|
||||||
},
|
|
||||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
|
||||||
command: func(cmd string, args ...string) *exec.Cmd {
|
|
||||||
// Handle git remote url call
|
|
||||||
if strings.HasPrefix(cmd, "git") {
|
|
||||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, cmd, "open")
|
|
||||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
test: func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
|
||||||
branch: &models.Branch{
|
|
||||||
Name: "feature/events",
|
|
||||||
},
|
|
||||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
|
||||||
command: func(cmd string, args ...string) *exec.Cmd {
|
|
||||||
// Handle git remote url call
|
|
||||||
if strings.HasPrefix(cmd, "git") {
|
|
||||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, cmd, "open")
|
|
||||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
test: func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: "Opens a link to new pull request on github",
|
|
||||||
branch: &models.Branch{
|
|
||||||
Name: "feature/sum-operation",
|
|
||||||
},
|
|
||||||
remoteUrl: "git@github.com:peter/calculator.git",
|
|
||||||
command: func(cmd string, args ...string) *exec.Cmd {
|
|
||||||
// Handle git remote url call
|
|
||||||
if strings.HasPrefix(cmd, "git") {
|
|
||||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, cmd, "open")
|
|
||||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
test: func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: "Opens a link to new pull request on gitlab",
|
|
||||||
branch: &models.Branch{
|
|
||||||
Name: "feature/ui",
|
|
||||||
},
|
|
||||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
|
||||||
command: func(cmd string, args ...string) *exec.Cmd {
|
|
||||||
// Handle git remote url call
|
|
||||||
if strings.HasPrefix(cmd, "git") {
|
|
||||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, cmd, "open")
|
|
||||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
|
||||||
return secureexec.Command("echo")
|
|
||||||
},
|
|
||||||
test: func(err error) {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testName: "Throws an error if git service is unsupported",
|
|
||||||
branch: &models.Branch{
|
|
||||||
Name: "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) {
|
|
||||||
assert.Error(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range scenarios {
|
|
||||||
t.Run(s.testName, func(t *testing.T) {
|
|
||||||
gitCommand := NewDummyGitCommand()
|
|
||||||
gitCommand.OSCommand.Command = s.command
|
|
||||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
|
||||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
|
||||||
// valid configuration for a custom service URL
|
|
||||||
"git.work.com": "gitlab:code.work.com",
|
|
||||||
// invalid configurations for a custom service URL
|
|
||||||
"invalid.work.com": "noservice:invalid.work.com",
|
|
||||||
"noservice.work.com": "noservice.work.com",
|
|
||||||
}
|
|
||||||
gitCommand.getGitConfigValue = func(path string) (string, error) {
|
|
||||||
assert.Equal(t, path, "remote.origin.url")
|
|
||||||
return s.remoteUrl, nil
|
|
||||||
}
|
|
||||||
dummyPullRequest := NewPullRequest(gitCommand)
|
|
||||||
s.test(dummyPullRequest.Create(s.branch))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCreatePullRequestOnWindows is a function.
|
||||||
|
func TestCreatePullRequestOnWindows(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
from string
|
||||||
|
to string
|
||||||
|
remoteUrl string
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
test func(url string, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket",
|
||||||
|
from: "feature/profile-page",
|
||||||
|
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||||
|
from: "feature/events",
|
||||||
|
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on github",
|
||||||
|
from: "feature/sum-operation",
|
||||||
|
remoteUrl: "git@github.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||||
|
from: "feature/profile-page/avatar",
|
||||||
|
to: "feature/profile-page",
|
||||||
|
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||||
|
from: "feature/remote-events",
|
||||||
|
to: "feature/events",
|
||||||
|
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on github with specific target branch",
|
||||||
|
from: "feature/sum-operation",
|
||||||
|
to: "feature/operations",
|
||||||
|
remoteUrl: "git@github.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab",
|
||||||
|
from: "feature/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||||
|
from: "feature/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||||
|
from: "feature/commit-ui",
|
||||||
|
to: "epic/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||||
|
from: "feature/commit-ui",
|
||||||
|
to: "epic/ui",
|
||||||
|
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
// Handle git remote url call
|
||||||
|
if strings.HasPrefix(cmd, "git") {
|
||||||
|
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, cmd, "cmd")
|
||||||
|
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Throws an error if git service is unsupported",
|
||||||
|
from: "feature/divide-operation",
|
||||||
|
remoteUrl: "git@something.com:peter/calculator.git",
|
||||||
|
command: func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
test: func(url string, err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
gitCommand := NewDummyGitCommand()
|
||||||
|
gitCommand.OSCommand.Command = s.command
|
||||||
|
gitCommand.OSCommand.Platform.OS = "windows"
|
||||||
|
gitCommand.OSCommand.Platform.Shell = "cmd"
|
||||||
|
gitCommand.OSCommand.Platform.ShellArg = "/c"
|
||||||
|
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}`
|
||||||
|
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||||
|
// valid configuration for a custom service URL
|
||||||
|
"git.work.com": "gitlab:code.work.com",
|
||||||
|
// invalid configurations for a custom service URL
|
||||||
|
"invalid.work.com": "noservice:invalid.work.com",
|
||||||
|
"noservice.work.com": "noservice.work.com",
|
||||||
|
}
|
||||||
|
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||||
|
dummyPullRequest := NewPullRequest(gitCommand)
|
||||||
|
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,8 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
|||||||
gitSequenceEditor := ex
|
gitSequenceEditor := ex
|
||||||
if todo == "" {
|
if todo == "" {
|
||||||
gitSequenceEditor = "true"
|
gitSequenceEditor = "true"
|
||||||
|
} else {
|
||||||
|
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Env = os.Environ()
|
cmd.Env = os.Environ()
|
||||||
@@ -117,7 +119,7 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI
|
|||||||
var commitAction string
|
var commitAction string
|
||||||
if i == actionIndex {
|
if i == actionIndex {
|
||||||
commitAction = action
|
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!
|
// 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.
|
// 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.
|
// 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:
|
// 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
|
// one where we handle the possibility of a credential request, and the other
|
||||||
// where we continue the rebase
|
// where we continue the rebase
|
||||||
if c.usingGpg() {
|
if c.UsingGpg() {
|
||||||
return errors.New(c.Tr.DisabledForGPG)
|
return errors.New(c.Tr.DisabledForGPG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
+15
-10
@@ -3,35 +3,40 @@ package commands
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
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", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) RemoveRemote(name string) error {
|
func (c *GitCommand) RemoveRemote(name string) error {
|
||||||
return c.OSCommand.RunCommand("git remote remove %s", name)
|
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
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", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
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", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
||||||
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
|
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
|
||||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
cmdObj := c.NewCmdObjFromStr(command)
|
||||||
|
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
|
||||||
|
return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRemoteBranchExists Returns remote branch
|
// CheckRemoteBranchExists Returns remote branch
|
||||||
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||||
_, err := c.OSCommand.RunCommandWithOutput(
|
_, err := c.OSCommand.RunCommandWithOutput(
|
||||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||||
branch.Name,
|
c.OSCommand.Quote(branchName),
|
||||||
)
|
)
|
||||||
|
|
||||||
return err == nil
|
return err == nil
|
||||||
@@ -39,5 +44,5 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
|||||||
|
|
||||||
// GetRemoteURL returns current repo remote url
|
// GetRemoteURL returns current repo remote url
|
||||||
func (c *GitCommand) GetRemoteURL() string {
|
func (c *GitCommand) GetRemoteURL() string {
|
||||||
return c.GetConfigValue("remote.origin.url")
|
return c.GitConfig.Get("remote.origin.url")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import "fmt"
|
|||||||
|
|
||||||
// StashDo modify stash
|
// StashDo modify stash
|
||||||
func (c *GitCommand) StashDo(index int, method string) error {
|
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
|
// StashSave save stash
|
||||||
// TODO: before calling this, check if there is anything to save
|
// TODO: before calling this, check if there is anything to save
|
||||||
func (c *GitCommand) StashSave(message string) error {
|
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
|
// 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
|
// 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
|
// 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 {
|
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||||
|
// wrap in 'writing', which uses a mutex
|
||||||
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
|
if err := c.RunCommand("git stash --keep-index"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
|||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
|||||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if file.ShortStatus == "AD" {
|
if file.ShortStatus == "AD" {
|
||||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
if err := c.UnStageFile(file.Names(), false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
+11
-11
@@ -69,28 +69,28 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
|||||||
return nil
|
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 {
|
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 {
|
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||||
// not doing an --init here because the user probably doesn't want that
|
// 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 {
|
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
// 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 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
|
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
|
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
|
// if the directory isn't there then that's fine
|
||||||
c.Log.Error(err)
|
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 {
|
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
|
// 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), c.OSCommand.Quote(newUrl)); err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
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 {
|
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 {
|
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||||
|
|||||||
+69
-34
@@ -2,42 +2,43 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
"github.com/go-errors/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
// Push pushes to a branch
|
||||||
// whether we need to run a subprocess to allow them to enter their password
|
type PushOpts struct {
|
||||||
func (c *GitCommand) usingGpg() bool {
|
Force bool
|
||||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
UpstreamRemote string
|
||||||
if overrideGpg {
|
UpstreamBranch string
|
||||||
return false
|
SetUpstream bool
|
||||||
}
|
PromptUserForCredential func(string) string
|
||||||
|
|
||||||
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(opts PushOpts) error {
|
||||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
cmdStr := "git push"
|
||||||
followTagsFlag := "--follow-tags"
|
|
||||||
if c.GetConfigValue("push.followTags") == "false" {
|
if opts.Force {
|
||||||
followTagsFlag = ""
|
cmdStr += " --force-with-lease"
|
||||||
}
|
}
|
||||||
|
|
||||||
forceFlag := ""
|
if opts.SetUpstream {
|
||||||
if force {
|
cmdStr += " --set-upstream"
|
||||||
forceFlag = "--force-with-lease"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUpstreamArg := ""
|
if opts.UpstreamRemote != "" {
|
||||||
if upstream != "" {
|
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote)
|
||||||
setUpstreamArg = "--set-upstream " + upstream
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
|
if opts.UpstreamBranch != "" {
|
||||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
if opts.UpstreamRemote == "" {
|
||||||
|
return errors.New(c.Tr.MustSpecifyOriginError)
|
||||||
|
}
|
||||||
|
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||||
|
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FetchOptions struct {
|
type FetchOptions struct {
|
||||||
@@ -48,16 +49,17 @@ type FetchOptions struct {
|
|||||||
|
|
||||||
// Fetch fetch git repo
|
// Fetch fetch git repo
|
||||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||||
command := "git fetch"
|
cmdStr := "git fetch"
|
||||||
|
|
||||||
if opts.RemoteName != "" {
|
if opts.RemoteName != "" {
|
||||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||||
}
|
}
|
||||||
if opts.BranchName != "" {
|
if opts.BranchName != "" {
|
||||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||||
|
return c.DetectUnamePass(cmdObj, func(question string) string {
|
||||||
if opts.PromptUserForCredential != nil {
|
if opts.PromptUserForCredential != nil {
|
||||||
return opts.PromptUserForCredential(question)
|
return opts.PromptUserForCredential(question)
|
||||||
}
|
}
|
||||||
@@ -65,12 +67,45 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PullOptions struct {
|
||||||
|
PromptUserForCredential func(string) string
|
||||||
|
RemoteName string
|
||||||
|
BranchName string
|
||||||
|
FastForwardOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GitCommand) Pull(opts PullOptions) error {
|
||||||
|
if opts.PromptUserForCredential == nil {
|
||||||
|
return errors.New("PromptUserForCredential is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdStr := "git pull --no-edit"
|
||||||
|
|
||||||
|
if opts.FastForwardOnly {
|
||||||
|
cmdStr += " --ff-only"
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.RemoteName != "" {
|
||||||
|
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||||
|
}
|
||||||
|
if opts.BranchName != "" {
|
||||||
|
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||||
|
// has 'pull.rebase = interactive' configured.
|
||||||
|
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
|
||||||
|
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
||||||
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||||
|
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
|
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
|
||||||
command := fmt.Sprintf("git fetch %s", remoteName)
|
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
|
||||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||||
|
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
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
|
||||||
|
command func(string, ...string) *exec.Cmd
|
||||||
|
opts PushOpts
|
||||||
|
test func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := func(passOrUname string) string {
|
||||||
|
return "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"Push with force disabled",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with force enabled",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{Force: true, PromptUserForCredential: prompt},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with an error occurring",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push"}, args)
|
||||||
|
return secureexec.Command("test")
|
||||||
|
},
|
||||||
|
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with force disabled, upstream supplied",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{
|
||||||
|
Force: false,
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
UpstreamBranch: "master",
|
||||||
|
PromptUserForCredential: prompt,
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with force disabled, setting upstream",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{
|
||||||
|
Force: false,
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
UpstreamBranch: "master",
|
||||||
|
PromptUserForCredential: prompt,
|
||||||
|
SetUpstream: true,
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with force enabled, setting upstream",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{
|
||||||
|
Force: true,
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
UpstreamBranch: "master",
|
||||||
|
PromptUserForCredential: prompt,
|
||||||
|
SetUpstream: true,
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with remote branch but no origin",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PushOpts{
|
||||||
|
Force: true,
|
||||||
|
UpstreamRemote: "",
|
||||||
|
UpstreamBranch: "master",
|
||||||
|
PromptUserForCredential: prompt,
|
||||||
|
SetUpstream: true,
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Push with force disabled, upstream supplied",
|
||||||
|
func(cmd string, args ...string) *exec.Cmd {
|
||||||
|
assert.EqualValues(t, "git", cmd)
|
||||||
|
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||||
|
|
||||||
|
return secureexec.Command("echo")
|
||||||
|
},
|
||||||
|
PushOpts{
|
||||||
|
Force: false,
|
||||||
|
UpstreamRemote: "origin",
|
||||||
|
UpstreamBranch: "master",
|
||||||
|
PromptUserForCredential: prompt,
|
||||||
|
},
|
||||||
|
func(err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
gitCmd := NewDummyGitCommand()
|
||||||
|
gitCmd.OSCommand.Command = s.command
|
||||||
|
err := gitCmd.Push(s.opts)
|
||||||
|
s.test(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
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", c.OSCommand.Quote(tagName), commitSha)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||||
return c.OSCommand.RunCommand("git tag -d %s", tagName)
|
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(tagName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
||||||
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
|
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
|
||||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||||
|
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-58
@@ -12,17 +12,19 @@ import (
|
|||||||
|
|
||||||
// AppConfig contains the base configuration fields required for lazygit.
|
// AppConfig contains the base configuration fields required for lazygit.
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||||
Commit string `long:"commit" env:"COMMIT"`
|
Commit string `long:"commit" env:"COMMIT"`
|
||||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||||
UserConfig *UserConfig
|
UserConfig *UserConfig
|
||||||
UserConfigDir string
|
UserConfigPaths []string
|
||||||
UserConfigPath string
|
DeafultConfFiles bool
|
||||||
AppState *AppState
|
UserConfigDir string
|
||||||
IsNewRepo bool
|
TempDir string
|
||||||
|
AppState *AppState
|
||||||
|
IsNewRepo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||||
@@ -35,23 +37,35 @@ type AppConfigurer interface {
|
|||||||
GetName() string
|
GetName() string
|
||||||
GetBuildSource() string
|
GetBuildSource() string
|
||||||
GetUserConfig() *UserConfig
|
GetUserConfig() *UserConfig
|
||||||
|
GetUserConfigPaths() []string
|
||||||
GetUserConfigDir() string
|
GetUserConfigDir() string
|
||||||
GetUserConfigPath() string
|
GetTempDir() string
|
||||||
GetAppState() *AppState
|
GetAppState() *AppState
|
||||||
SaveAppState() error
|
SaveAppState() error
|
||||||
SetIsNewRepo(bool)
|
SetIsNewRepo(bool)
|
||||||
GetIsNewRepo() bool
|
GetIsNewRepo() bool
|
||||||
ReloadUserConfig() error
|
ReloadUserConfig() error
|
||||||
|
ShowCommandLogOnStartup() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppConfig makes a new app config
|
// NewAppConfig makes a new app config
|
||||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
||||||
configDir, err := findOrCreateConfigDir()
|
configDir, err := findOrCreateConfigDir()
|
||||||
if err != nil {
|
if err != nil && !os.IsPermission(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userConfig, err := loadUserConfigWithDefaults(configDir)
|
var userConfigPaths []string
|
||||||
|
customConfigFiles := os.Getenv("LG_CONFIG_FILE")
|
||||||
|
if customConfigFiles != "" {
|
||||||
|
// Load user defined config files
|
||||||
|
userConfigPaths = strings.Split(customConfigFiles, ",")
|
||||||
|
} else {
|
||||||
|
// Load default config files
|
||||||
|
userConfigPaths = []string{filepath.Join(configDir, ConfigFilename)}
|
||||||
|
}
|
||||||
|
|
||||||
|
userConfig, err := loadUserConfigWithDefaults(userConfigPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -60,28 +74,35 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
|||||||
debuggingFlag = true
|
debuggingFlag = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tempDir := filepath.Join(os.TempDir(), "lazygit")
|
||||||
|
|
||||||
appState, err := loadAppState()
|
appState, err := loadAppState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
appConfig := &AppConfig{
|
appConfig := &AppConfig{
|
||||||
Name: "lazygit",
|
Name: "lazygit",
|
||||||
Version: version,
|
Version: version,
|
||||||
Commit: commit,
|
Commit: commit,
|
||||||
BuildDate: date,
|
BuildDate: date,
|
||||||
Debug: debuggingFlag,
|
Debug: debuggingFlag,
|
||||||
BuildSource: buildSource,
|
BuildSource: buildSource,
|
||||||
UserConfig: userConfig,
|
UserConfig: userConfig,
|
||||||
UserConfigDir: configDir,
|
UserConfigPaths: userConfigPaths,
|
||||||
UserConfigPath: filepath.Join(configDir, "config.yml"),
|
UserConfigDir: configDir,
|
||||||
AppState: appState,
|
TempDir: tempDir,
|
||||||
IsNewRepo: false,
|
AppState: appState,
|
||||||
|
IsNewRepo: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return appConfig, nil
|
return appConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCustomConfigFile(path string) bool {
|
||||||
|
return path != filepath.Join(ConfigDir(), ConfigFilename)
|
||||||
|
}
|
||||||
|
|
||||||
func ConfigDir() string {
|
func ConfigDir() string {
|
||||||
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
||||||
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||||
@@ -102,43 +123,45 @@ func configDirForVendor(vendor string) string {
|
|||||||
|
|
||||||
func findOrCreateConfigDir() (string, error) {
|
func findOrCreateConfigDir() (string, error) {
|
||||||
folder := ConfigDir()
|
folder := ConfigDir()
|
||||||
err := os.MkdirAll(folder, 0755)
|
return folder, os.MkdirAll(folder, 0755)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return folder, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
|
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
|
||||||
return loadUserConfig(configDir, GetDefaultConfig())
|
return loadUserConfig(configFiles, GetDefaultConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error) {
|
||||||
fileName := filepath.Join(configDir, "config.yml")
|
for _, path := range configFiles {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(fileName); err != nil {
|
// if use has supplied their own custom config file path(s), we assume
|
||||||
if os.IsNotExist(err) {
|
// the files have already been created, so we won't go and create them here.
|
||||||
file, err := os.Create(fileName)
|
if isCustomConfigFile(path) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "read-only file system") {
|
if os.IsPermission(err) {
|
||||||
return base, nil
|
// apparently when people have read-only permissions they prefer us to fail silently
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
file.Close()
|
file.Close()
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
content, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
content, err := ioutil.ReadFile(fileName)
|
if err := yaml.Unmarshal(content, base); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(content, base); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return base, nil
|
return base, nil
|
||||||
@@ -190,22 +213,25 @@ func (c *AppConfig) GetUserConfig() *UserConfig {
|
|||||||
return c.UserConfig
|
return c.UserConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserConfig returns the user config
|
|
||||||
func (c *AppConfig) GetUserConfigPath() string {
|
|
||||||
return c.UserConfigPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppState returns the app state
|
// GetAppState returns the app state
|
||||||
func (c *AppConfig) GetAppState() *AppState {
|
func (c *AppConfig) GetAppState() *AppState {
|
||||||
return c.AppState
|
return c.AppState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AppConfig) GetUserConfigPaths() []string {
|
||||||
|
return c.UserConfigPaths
|
||||||
|
}
|
||||||
|
|
||||||
func (c *AppConfig) GetUserConfigDir() string {
|
func (c *AppConfig) GetUserConfigDir() string {
|
||||||
return c.UserConfigDir
|
return c.UserConfigDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *AppConfig) GetTempDir() string {
|
||||||
|
return c.TempDir
|
||||||
|
}
|
||||||
|
|
||||||
func (c *AppConfig) ReloadUserConfig() error {
|
func (c *AppConfig) ReloadUserConfig() error {
|
||||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -223,9 +249,11 @@ func configFilePath(filename string) (string, error) {
|
|||||||
return filepath.Join(folder, filename), nil
|
return filepath.Join(folder, filename), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFilename returns the filename of the current config file
|
var ConfigFilename = "config.yml"
|
||||||
|
|
||||||
|
// ConfigFilename returns the filename of the deafult config file
|
||||||
func (c *AppConfig) ConfigFilename() string {
|
func (c *AppConfig) ConfigFilename() string {
|
||||||
return filepath.Join(c.UserConfigDir, "config.yml")
|
return filepath.Join(c.UserConfigDir, ConfigFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAppState marshalls the AppState struct and writes it to the disk
|
// SaveAppState marshalls the AppState struct and writes it to the disk
|
||||||
@@ -240,13 +268,34 @@ func (c *AppConfig) SaveAppState() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||||
|
if err != nil && os.IsPermission(err) {
|
||||||
|
// apparently when people have read-only permissions they prefer us to fail silently
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// originally we could only hide the command log permanently via the config
|
||||||
|
// but now we do it via state. So we need to still support the config for the
|
||||||
|
// sake of backwards compatibility
|
||||||
|
func (c *AppConfig) ShowCommandLogOnStartup() bool {
|
||||||
|
if !c.UserConfig.Gui.ShowCommandLog {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !c.AppState.HideCommandLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadAppState loads recorded AppState from file
|
// loadAppState loads recorded AppState from file
|
||||||
func loadAppState() (*AppState, error) {
|
func loadAppState() (*AppState, error) {
|
||||||
filepath, err := configFilePath("state.yml")
|
filepath, err := configFilePath("state.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
// apparently when people have read-only permissions they prefer us to fail silently
|
||||||
|
return getDefaultAppState(), nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +323,10 @@ type AppState struct {
|
|||||||
LastUpdateCheck int64
|
LastUpdateCheck int64
|
||||||
RecentRepos []string
|
RecentRepos []string
|
||||||
StartupPopupVersion int
|
StartupPopupVersion int
|
||||||
|
|
||||||
|
// these are for custom commands typed in directly, not for custom commands in the lazygit config
|
||||||
|
CustomCommandsHistory []string
|
||||||
|
HideCommandLog bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultAppState() *AppState {
|
func getDefaultAppState() *AppState {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build !windows && !linux
|
||||||
// +build !windows,!linux
|
// +build !windows,!linux
|
||||||
|
|
||||||
package config
|
package config
|
||||||
@@ -5,7 +6,9 @@ package config
|
|||||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||||
func GetPlatformDefaultConfig() OSConfig {
|
func GetPlatformDefaultConfig() OSConfig {
|
||||||
return OSConfig{
|
return OSConfig{
|
||||||
OpenCommand: "open {{filename}}",
|
EditCommand: ``,
|
||||||
OpenLinkCommand: "open {{link}}",
|
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||||
|
OpenCommand: "open {{filename}}",
|
||||||
|
OpenLinkCommand: "open {{link}}",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package config
|
|||||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||||
func GetPlatformDefaultConfig() OSConfig {
|
func GetPlatformDefaultConfig() OSConfig {
|
||||||
return OSConfig{
|
return OSConfig{
|
||||||
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
|
EditCommand: ``,
|
||||||
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
|
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||||
|
OpenCommand: `xdg-open {{filename}} >/dev/null`,
|
||||||
|
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package config
|
|||||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||||
func GetPlatformDefaultConfig() OSConfig {
|
func GetPlatformDefaultConfig() OSConfig {
|
||||||
return OSConfig{
|
return OSConfig{
|
||||||
OpenCommand: `cmd /c "start "" {{filename}}"`,
|
EditCommand: ``,
|
||||||
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
|
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||||
|
OpenCommand: `start "" {{filename}}`,
|
||||||
|
OpenLinkCommand: `start "" {{link}}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func NewDummyAppConfig() *AppConfig {
|
|||||||
Debug: false,
|
Debug: false,
|
||||||
BuildSource: "",
|
BuildSource: "",
|
||||||
UserConfig: GetDefaultConfig(),
|
UserConfig: GetDefaultConfig(),
|
||||||
|
AppState: &AppState{},
|
||||||
}
|
}
|
||||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||||
return appConfig
|
return appConfig
|
||||||
|
|||||||
+134
-73
@@ -24,6 +24,7 @@ type RefresherConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GuiConfig struct {
|
type GuiConfig struct {
|
||||||
|
AuthorColors map[string]string `yaml:"authorColors"`
|
||||||
ScrollHeight int `yaml:"scrollHeight"`
|
ScrollHeight int `yaml:"scrollHeight"`
|
||||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||||
MouseEvents bool `yaml:"mouseEvents"`
|
MouseEvents bool `yaml:"mouseEvents"`
|
||||||
@@ -32,18 +33,26 @@ type GuiConfig struct {
|
|||||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||||
|
Language string `yaml:"language"`
|
||||||
Theme ThemeConfig `yaml:"theme"`
|
Theme ThemeConfig `yaml:"theme"`
|
||||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
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 {
|
type ThemeConfig struct {
|
||||||
LightTheme bool `yaml:"lightTheme"`
|
LightTheme bool `yaml:"lightTheme"`
|
||||||
ActiveBorderColor []string `yaml:"activeBorderColor"`
|
ActiveBorderColor []string `yaml:"activeBorderColor"`
|
||||||
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
|
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
|
||||||
OptionsTextColor []string `yaml:"optionsTextColor"`
|
OptionsTextColor []string `yaml:"optionsTextColor"`
|
||||||
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
|
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
|
||||||
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
||||||
|
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
|
||||||
|
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommitLengthConfig struct {
|
type CommitLengthConfig struct {
|
||||||
@@ -53,7 +62,6 @@ type CommitLengthConfig struct {
|
|||||||
type GitConfig struct {
|
type GitConfig struct {
|
||||||
Paging PagingConfig `yaml:"paging"`
|
Paging PagingConfig `yaml:"paging"`
|
||||||
Merging MergingConfig `yaml:"merging"`
|
Merging MergingConfig `yaml:"merging"`
|
||||||
Pull PullConfig `yaml:"pull"`
|
|
||||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||||
AutoFetch bool `yaml:"autoFetch"`
|
AutoFetch bool `yaml:"autoFetch"`
|
||||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||||
@@ -61,6 +69,9 @@ type GitConfig struct {
|
|||||||
OverrideGpg bool `yaml:"overrideGpg"`
|
OverrideGpg bool `yaml:"overrideGpg"`
|
||||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||||
|
// this shoudl really be under 'gui', not 'git'
|
||||||
|
ParseEmoji bool `yaml:"parseEmoji"`
|
||||||
|
Log LogConfig `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PagingConfig struct {
|
type PagingConfig struct {
|
||||||
@@ -74,8 +85,9 @@ type MergingConfig struct {
|
|||||||
Args string `yaml:"args"`
|
Args string `yaml:"args"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PullConfig struct {
|
type LogConfig struct {
|
||||||
Mode string `yaml:"mode"`
|
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
|
||||||
|
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommitPrefixConfig struct {
|
type CommitPrefixConfig struct {
|
||||||
@@ -100,61 +112,70 @@ type KeybindingConfig struct {
|
|||||||
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
|
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||||
type KeybindingUniversalConfig struct {
|
type KeybindingUniversalConfig struct {
|
||||||
Quit string `yaml:"quit"`
|
Quit string `yaml:"quit"`
|
||||||
QuitAlt1 string `yaml:"quit-alt1"`
|
QuitAlt1 string `yaml:"quit-alt1"`
|
||||||
Return string `yaml:"return"`
|
Return string `yaml:"return"`
|
||||||
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
|
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
|
||||||
TogglePanel string `yaml:"togglePanel"`
|
TogglePanel string `yaml:"togglePanel"`
|
||||||
PrevItem string `yaml:"prevItem"`
|
PrevItem string `yaml:"prevItem"`
|
||||||
NextItem string `yaml:"nextItem"`
|
NextItem string `yaml:"nextItem"`
|
||||||
PrevItemAlt string `yaml:"prevItem-alt"`
|
PrevItemAlt string `yaml:"prevItem-alt"`
|
||||||
NextItemAlt string `yaml:"nextItem-alt"`
|
NextItemAlt string `yaml:"nextItem-alt"`
|
||||||
PrevPage string `yaml:"prevPage"`
|
PrevPage string `yaml:"prevPage"`
|
||||||
NextPage string `yaml:"nextPage"`
|
NextPage string `yaml:"nextPage"`
|
||||||
GotoTop string `yaml:"gotoTop"`
|
ScrollLeft string `yaml:"scrollLeft"`
|
||||||
GotoBottom string `yaml:"gotoBottom"`
|
ScrollRight string `yaml:"scrollRight"`
|
||||||
PrevBlock string `yaml:"prevBlock"`
|
GotoTop string `yaml:"gotoTop"`
|
||||||
NextBlock string `yaml:"nextBlock"`
|
GotoBottom string `yaml:"gotoBottom"`
|
||||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
PrevBlock string `yaml:"prevBlock"`
|
||||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
NextBlock string `yaml:"nextBlock"`
|
||||||
NextMatch string `yaml:"nextMatch"`
|
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||||
PrevMatch string `yaml:"prevMatch"`
|
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||||
StartSearch string `yaml:"startSearch"`
|
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||||
OptionMenu string `yaml:"optionMenu"`
|
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||||
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
|
JumpToBlock []string `yaml:"jumpToBlock"`
|
||||||
Select string `yaml:"select"`
|
NextMatch string `yaml:"nextMatch"`
|
||||||
GoInto string `yaml:"goInto"`
|
PrevMatch string `yaml:"prevMatch"`
|
||||||
Confirm string `yaml:"confirm"`
|
StartSearch string `yaml:"startSearch"`
|
||||||
ConfirmAlt1 string `yaml:"confirm-alt1"`
|
OptionMenu string `yaml:"optionMenu"`
|
||||||
Remove string `yaml:"remove"`
|
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
|
||||||
New string `yaml:"new"`
|
Select string `yaml:"select"`
|
||||||
Edit string `yaml:"edit"`
|
GoInto string `yaml:"goInto"`
|
||||||
OpenFile string `yaml:"openFile"`
|
Confirm string `yaml:"confirm"`
|
||||||
ScrollUpMain string `yaml:"scrollUpMain"`
|
ConfirmAlt1 string `yaml:"confirm-alt1"`
|
||||||
ScrollDownMain string `yaml:"scrollDownMain"`
|
Remove string `yaml:"remove"`
|
||||||
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
|
New string `yaml:"new"`
|
||||||
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
|
Edit string `yaml:"edit"`
|
||||||
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
|
OpenFile string `yaml:"openFile"`
|
||||||
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
|
ScrollUpMain string `yaml:"scrollUpMain"`
|
||||||
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
|
ScrollDownMain string `yaml:"scrollDownMain"`
|
||||||
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
|
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
|
||||||
PushFiles string `yaml:"pushFiles"`
|
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
|
||||||
PullFiles string `yaml:"pullFiles"`
|
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
|
||||||
Refresh string `yaml:"refresh"`
|
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
|
||||||
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
|
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
|
||||||
NextTab string `yaml:"nextTab"`
|
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
|
||||||
PrevTab string `yaml:"prevTab"`
|
PushFiles string `yaml:"pushFiles"`
|
||||||
NextScreenMode string `yaml:"nextScreenMode"`
|
PullFiles string `yaml:"pullFiles"`
|
||||||
PrevScreenMode string `yaml:"prevScreenMode"`
|
Refresh string `yaml:"refresh"`
|
||||||
Undo string `yaml:"undo"`
|
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
|
||||||
Redo string `yaml:"redo"`
|
NextTab string `yaml:"nextTab"`
|
||||||
FilteringMenu string `yaml:"filteringMenu"`
|
PrevTab string `yaml:"prevTab"`
|
||||||
DiffingMenu string `yaml:"diffingMenu"`
|
NextScreenMode string `yaml:"nextScreenMode"`
|
||||||
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
PrevScreenMode string `yaml:"prevScreenMode"`
|
||||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
Undo string `yaml:"undo"`
|
||||||
SubmitEditorText string `yaml:"submitEditorText"`
|
Redo string `yaml:"redo"`
|
||||||
AppendNewline string `yaml:"appendNewline"`
|
FilteringMenu string `yaml:"filteringMenu"`
|
||||||
|
DiffingMenu string `yaml:"diffingMenu"`
|
||||||
|
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
||||||
|
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||||
|
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||||
|
SubmitEditorText string `yaml:"submitEditorText"`
|
||||||
|
AppendNewline string `yaml:"appendNewline"`
|
||||||
|
ExtrasMenu string `yaml:"extrasMenu"`
|
||||||
|
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingStatusConfig struct {
|
type KeybindingStatusConfig struct {
|
||||||
@@ -175,10 +196,14 @@ type KeybindingFilesConfig struct {
|
|||||||
ToggleStagedAll string `yaml:"toggleStagedAll"`
|
ToggleStagedAll string `yaml:"toggleStagedAll"`
|
||||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||||
Fetch string `yaml:"fetch"`
|
Fetch string `yaml:"fetch"`
|
||||||
|
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||||
|
OpenMergeTool string `yaml:"openMergeTool"`
|
||||||
|
OpenStatusFilter string `yaml:"openStatusFilter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingBranchesConfig struct {
|
type KeybindingBranchesConfig struct {
|
||||||
CreatePullRequest string `yaml:"createPullRequest"`
|
CreatePullRequest string `yaml:"createPullRequest"`
|
||||||
|
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
|
||||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||||
@@ -212,6 +237,7 @@ type KeybindingCommitsConfig struct {
|
|||||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||||
|
OpenLogMenu string `yaml:"openLogMenu"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingStashConfig struct {
|
type KeybindingStashConfig struct {
|
||||||
@@ -237,6 +263,12 @@ type KeybindingSubmodulesConfig struct {
|
|||||||
|
|
||||||
// OSConfig contains config on the level of the os
|
// OSConfig contains config on the level of the os
|
||||||
type OSConfig struct {
|
type OSConfig struct {
|
||||||
|
// EditCommand is the command for editing a file
|
||||||
|
EditCommand string `yaml:"editCommand,omitempty"`
|
||||||
|
|
||||||
|
// EditCommandTemplate is the command template for editing a file
|
||||||
|
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
|
||||||
|
|
||||||
// OpenCommand is the command for opening a file
|
// OpenCommand is the command for opening a file
|
||||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||||
|
|
||||||
@@ -263,6 +295,12 @@ type CustomCommandPrompt struct {
|
|||||||
|
|
||||||
// this only applies to menus
|
// this only applies to menus
|
||||||
Options []CustomCommandMenuOption
|
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 {
|
type CustomCommandMenuOption struct {
|
||||||
@@ -282,16 +320,24 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
SidePanelWidth: 0.3333,
|
SidePanelWidth: 0.3333,
|
||||||
ExpandFocusedSidePanel: false,
|
ExpandFocusedSidePanel: false,
|
||||||
MainPanelSplitMode: "flexible",
|
MainPanelSplitMode: "flexible",
|
||||||
|
Language: "auto",
|
||||||
Theme: ThemeConfig{
|
Theme: ThemeConfig{
|
||||||
LightTheme: false,
|
LightTheme: false,
|
||||||
ActiveBorderColor: []string{"green", "bold"},
|
ActiveBorderColor: []string{"green", "bold"},
|
||||||
InactiveBorderColor: []string{"white"},
|
InactiveBorderColor: []string{"white"},
|
||||||
OptionsTextColor: []string{"blue"},
|
OptionsTextColor: []string{"blue"},
|
||||||
SelectedLineBgColor: []string{"default"},
|
SelectedLineBgColor: []string{"default"},
|
||||||
SelectedRangeBgColor: []string{"blue"},
|
SelectedRangeBgColor: []string{"blue"},
|
||||||
|
CherryPickedCommitBgColor: []string{"blue"},
|
||||||
|
CherryPickedCommitFgColor: []string{"cyan"},
|
||||||
},
|
},
|
||||||
CommitLength: CommitLengthConfig{Show: true},
|
CommitLength: CommitLengthConfig{Show: true},
|
||||||
SkipNoStagedFilesWarning: false,
|
SkipNoStagedFilesWarning: false,
|
||||||
|
ShowListFooter: true,
|
||||||
|
ShowCommandLog: true,
|
||||||
|
ShowFileTree: true,
|
||||||
|
ShowRandomTip: true,
|
||||||
|
CommandLogSize: 8,
|
||||||
},
|
},
|
||||||
Git: GitConfig{
|
Git: GitConfig{
|
||||||
Paging: PagingConfig{
|
Paging: PagingConfig{
|
||||||
@@ -302,8 +348,9 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
ManualCommit: false,
|
ManualCommit: false,
|
||||||
Args: "",
|
Args: "",
|
||||||
},
|
},
|
||||||
Pull: PullConfig{
|
Log: LogConfig{
|
||||||
Mode: "merge",
|
Order: "topo-order",
|
||||||
|
ShowGraph: "when-maximised",
|
||||||
},
|
},
|
||||||
SkipHookPrefix: "WIP",
|
SkipHookPrefix: "WIP",
|
||||||
AutoFetch: true,
|
AutoFetch: true,
|
||||||
@@ -311,6 +358,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||||
DisableForcePushing: false,
|
DisableForcePushing: false,
|
||||||
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
||||||
|
ParseEmoji: false,
|
||||||
},
|
},
|
||||||
Refresher: RefresherConfig{
|
Refresher: RefresherConfig{
|
||||||
RefreshInterval: 10,
|
RefreshInterval: 10,
|
||||||
@@ -323,7 +371,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
Reporting: "undetermined",
|
Reporting: "undetermined",
|
||||||
SplashUpdatesIndex: 0,
|
SplashUpdatesIndex: 0,
|
||||||
ConfirmOnQuit: false,
|
ConfirmOnQuit: false,
|
||||||
QuitOnTopLevelReturn: true,
|
QuitOnTopLevelReturn: false,
|
||||||
Keybinding: KeybindingConfig{
|
Keybinding: KeybindingConfig{
|
||||||
Universal: KeybindingUniversalConfig{
|
Universal: KeybindingUniversalConfig{
|
||||||
Quit: "q",
|
Quit: "q",
|
||||||
@@ -337,12 +385,17 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
NextItemAlt: "j",
|
NextItemAlt: "j",
|
||||||
PrevPage: ",",
|
PrevPage: ",",
|
||||||
NextPage: ".",
|
NextPage: ".",
|
||||||
|
ScrollLeft: "H",
|
||||||
|
ScrollRight: "L",
|
||||||
GotoTop: "<",
|
GotoTop: "<",
|
||||||
GotoBottom: ">",
|
GotoBottom: ">",
|
||||||
PrevBlock: "<left>",
|
PrevBlock: "<left>",
|
||||||
NextBlock: "<right>",
|
NextBlock: "<right>",
|
||||||
PrevBlockAlt: "h",
|
PrevBlockAlt: "h",
|
||||||
NextBlockAlt: "l",
|
NextBlockAlt: "l",
|
||||||
|
PrevBlockAlt2: "<backtab>",
|
||||||
|
NextBlockAlt2: "<tab>",
|
||||||
|
JumpToBlock: []string{"1", "2", "3", "4", "5"},
|
||||||
NextMatch: "n",
|
NextMatch: "n",
|
||||||
PrevMatch: "N",
|
PrevMatch: "N",
|
||||||
StartSearch: "/",
|
StartSearch: "/",
|
||||||
@@ -356,6 +409,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
New: "n",
|
New: "n",
|
||||||
Edit: "e",
|
Edit: "e",
|
||||||
OpenFile: "o",
|
OpenFile: "o",
|
||||||
|
OpenRecentRepos: "<c-r>",
|
||||||
ScrollUpMain: "<pgup>",
|
ScrollUpMain: "<pgup>",
|
||||||
ScrollDownMain: "<pgdown>",
|
ScrollDownMain: "<pgdown>",
|
||||||
ScrollUpMainAlt1: "K",
|
ScrollUpMainAlt1: "K",
|
||||||
@@ -379,7 +433,9 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
DiffingMenuAlt: "<c-e>",
|
DiffingMenuAlt: "<c-e>",
|
||||||
CopyToClipboard: "<c-o>",
|
CopyToClipboard: "<c-o>",
|
||||||
SubmitEditorText: "<enter>",
|
SubmitEditorText: "<enter>",
|
||||||
AppendNewline: "<tab>",
|
AppendNewline: "<a-enter>",
|
||||||
|
ExtrasMenu: "@",
|
||||||
|
ToggleWhitespaceInDiffView: "<c-w>",
|
||||||
},
|
},
|
||||||
Status: KeybindingStatusConfig{
|
Status: KeybindingStatusConfig{
|
||||||
CheckForUpdate: "u",
|
CheckForUpdate: "u",
|
||||||
@@ -398,10 +454,14 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
ToggleStagedAll: "a",
|
ToggleStagedAll: "a",
|
||||||
ViewResetOptions: "D",
|
ViewResetOptions: "D",
|
||||||
Fetch: "f",
|
Fetch: "f",
|
||||||
|
ToggleTreeView: "`",
|
||||||
|
OpenMergeTool: "M",
|
||||||
|
OpenStatusFilter: "<c-b>",
|
||||||
},
|
},
|
||||||
Branches: KeybindingBranchesConfig{
|
Branches: KeybindingBranchesConfig{
|
||||||
CopyPullRequestURL: "<c-y>",
|
CopyPullRequestURL: "<c-y>",
|
||||||
CreatePullRequest: "o",
|
CreatePullRequest: "o",
|
||||||
|
ViewPullRequestOptions: "O",
|
||||||
CheckoutBranchByName: "c",
|
CheckoutBranchByName: "c",
|
||||||
ForceCheckoutBranch: "F",
|
ForceCheckoutBranch: "F",
|
||||||
RebaseBranch: "r",
|
RebaseBranch: "r",
|
||||||
@@ -433,6 +493,7 @@ func GetDefaultConfig() *UserConfig {
|
|||||||
CheckoutCommit: "<space>",
|
CheckoutCommit: "<space>",
|
||||||
ResetCherryPick: "<c-R>",
|
ResetCherryPick: "<c-R>",
|
||||||
CopyCommitMessageToClipboard: "<c-y>",
|
CopyCommitMessageToClipboard: "<c-y>",
|
||||||
|
OpenLogMenu: "<c-l>",
|
||||||
},
|
},
|
||||||
Stash: KeybindingStashConfig{
|
Stash: KeybindingStashConfig{
|
||||||
PopStash: "g",
|
PopStash: "g",
|
||||||
|
|||||||
@@ -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 {
|
for range ticker.C {
|
||||||
appStatus := gui.statusManager.getStatusString()
|
appStatus := gui.statusManager.getStatusString()
|
||||||
if appStatus == "" {
|
if appStatus == "" {
|
||||||
gui.renderString("appStatus", "")
|
gui.renderString(gui.Views.AppStatus, "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gui.renderString("appStatus", appStatus)
|
gui.renderString(gui.Views.AppStatus, appStatus)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-14
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const INFO_SECTION_PADDING = " "
|
||||||
|
|
||||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||||
currentWindow := gui.currentWindow()
|
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 {
|
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||||
width, height := gui.g.Size()
|
width, height := gui.g.Size()
|
||||||
|
|
||||||
@@ -146,6 +166,8 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||||||
mainPanelsDirection = boxlayout.COLUMN
|
mainPanelsDirection = boxlayout.COLUMN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extrasWindowSize := gui.getExtrasWindowSize(height)
|
||||||
|
|
||||||
root := &boxlayout.Box{
|
root := &boxlayout.Box{
|
||||||
Direction: boxlayout.ROW,
|
Direction: boxlayout.ROW,
|
||||||
Children: []*boxlayout.Box{
|
Children: []*boxlayout.Box{
|
||||||
@@ -159,9 +181,19 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||||||
ConditionalChildren: gui.sidePanelChildren,
|
ConditionalChildren: gui.sidePanelChildren,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Direction: mainPanelsDirection,
|
Direction: boxlayout.ROW,
|
||||||
Weight: mainSectionWeight,
|
Weight: mainSectionWeight,
|
||||||
Children: gui.mainSectionChildren(),
|
Children: []*boxlayout.Box{
|
||||||
|
{
|
||||||
|
Direction: mainPanelsDirection,
|
||||||
|
Children: gui.mainSectionChildren(),
|
||||||
|
Weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Window: "extras",
|
||||||
|
Size: extrasWindowSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -178,12 +210,15 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
|||||||
|
|
||||||
// The stash window by default only contains one line so that it's not hogging
|
// The stash window by default only contains one line so that it's not hogging
|
||||||
// too much space, but if you access it it should take up some space. This is
|
// too much space, but if you access it it should take up some space. This is
|
||||||
// the default behaviour when accordian mode is NOT in effect. If it is in effect
|
// the default behaviour when accordion mode is NOT in effect. If it is in effect
|
||||||
// then when it's accessed it will have weight 2, not 1.
|
// then when it's accessed it will have weight 2, not 1.
|
||||||
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
|
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
|
||||||
|
gui.State.ContextManager.RLock()
|
||||||
|
defer gui.State.ContextManager.RUnlock()
|
||||||
|
|
||||||
box := &boxlayout.Box{Window: "stash"}
|
box := &boxlayout.Box{Window: "stash"}
|
||||||
stashWindowAccessed := false
|
stashWindowAccessed := false
|
||||||
for _, context := range gui.State.ContextStack {
|
for _, context := range gui.State.ContextManager.ContextStack {
|
||||||
if context.GetWindowName() == "stash" {
|
if context.GetWindowName() == "stash" {
|
||||||
stashWindowAccessed = true
|
stashWindowAccessed = true
|
||||||
}
|
}
|
||||||
@@ -224,9 +259,9 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||||||
fullHeightBox("stash"),
|
fullHeightBox("stash"),
|
||||||
}
|
}
|
||||||
} else if height >= 28 {
|
} else if height >= 28 {
|
||||||
accordianMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||||
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||||
if accordianMode && defaultBox.Window == currentWindow {
|
if accordionMode && defaultBox.Window == currentWindow {
|
||||||
return &boxlayout.Box{
|
return &boxlayout.Box{
|
||||||
Window: defaultBox.Window,
|
Window: defaultBox.Window,
|
||||||
Weight: 2,
|
Weight: 2,
|
||||||
@@ -241,10 +276,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||||||
Window: "status",
|
Window: "status",
|
||||||
Size: 3,
|
Size: 3,
|
||||||
},
|
},
|
||||||
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
||||||
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
||||||
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
||||||
accordianBox(gui.getDefaultStashWindowBox()),
|
accordionBox(gui.getDefaultStashWindowBox()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
squashedHeight := 1
|
squashedHeight := 1
|
||||||
@@ -278,9 +313,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
|||||||
|
|
||||||
func (gui *Gui) currentSideWindowName() string {
|
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
|
// 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 {
|
gui.State.ContextManager.RLock()
|
||||||
reversedIdx := len(gui.State.ContextStack) - 1 - idx
|
defer gui.State.ContextManager.RUnlock()
|
||||||
context := gui.State.ContextStack[reversedIdx]
|
|
||||||
|
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 {
|
if context.GetKind() == SIDE_CONTEXT {
|
||||||
return context.GetWindowName()
|
return context.GetWindowName()
|
||||||
|
|||||||
@@ -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
|
Y1 int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Direction int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ROW = iota
|
ROW Direction = iota
|
||||||
COLUMN
|
COLUMN
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,10 +28,10 @@ const (
|
|||||||
|
|
||||||
type Box struct {
|
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 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
|
// 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
|
Children []*Box
|
||||||
|
|
||||||
@@ -94,6 +96,11 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
|||||||
var boxSize int
|
var boxSize int
|
||||||
if child.isStatic() {
|
if child.isStatic() {
|
||||||
boxSize = child.Size
|
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 {
|
} else {
|
||||||
// TODO: consider more evenly distributing the remainder
|
// TODO: consider more evenly distributing the remainder
|
||||||
boxSize = unitSize * child.Weight
|
boxSize = unitSize * child.Weight
|
||||||
@@ -120,7 +127,7 @@ func (b *Box) isStatic() bool {
|
|||||||
return b.Size > 0
|
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 {
|
if b.ConditionalDirection != nil {
|
||||||
return b.ConditionalDirection(width, height)
|
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 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 {
|
if width > 4 {
|
||||||
return COLUMN
|
return COLUMN
|
||||||
} else {
|
} else {
|
||||||
@@ -111,7 +111,7 @@ func TestArrangeWindows(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Box with COLUMN direction only on wide boxes with wide box",
|
"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 {
|
if width > 4 {
|
||||||
return COLUMN
|
return COLUMN
|
||||||
} else {
|
} 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 {
|
for _, s := range scenarios {
|
||||||
|
|||||||
+82
-96
@@ -4,11 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,13 +29,13 @@ func (gui *Gui) handleBranchSelect() error {
|
|||||||
var task updateTask
|
var task updateTask
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
if branch == nil {
|
if branch == nil {
|
||||||
task = gui.createRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||||
} else {
|
} else {
|
||||||
cmd := gui.OSCommand.ExecutableFromString(
|
cmd := gui.OSCommand.ExecutableFromString(
|
||||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||||
)
|
)
|
||||||
|
|
||||||
task = gui.createRunPtyTask(cmd)
|
task = NewRunPtyTask(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.refreshMainViews(refreshMainOpts{
|
return gui.refreshMainViews(refreshMainOpts{
|
||||||
@@ -70,7 +68,7 @@ func (gui *Gui) refreshBranches() {
|
|||||||
}
|
}
|
||||||
gui.State.Branches = builder.Build()
|
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)
|
gui.Log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +77,7 @@ func (gui *Gui) refreshBranches() {
|
|||||||
|
|
||||||
// specific functions
|
// 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 {
|
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -87,46 +85,52 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
|
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
|
||||||
}
|
}
|
||||||
branch := gui.getSelectedBranch()
|
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)
|
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||||
|
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
if err := pullRequest.Create(branch); err != nil {
|
url, err := pullRequest.CopyURL(branch.Name, "")
|
||||||
return gui.surfaceError(err)
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) handleCopyPullRequestURLPress(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
|
||||||
|
|
||||||
branch := gui.getSelectedBranch()
|
|
||||||
if err := pullRequest.CopyURL(branch); err != nil {
|
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false))
|
||||||
|
|
||||||
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
|
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
|
||||||
|
|
||||||
return nil
|
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 {
|
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
go utils.Safe(func() {
|
go utils.Safe(func() {
|
||||||
err := gui.fetch(true)
|
err := gui.fetch(true, "Fetch")
|
||||||
gui.handleCredentialsPopup(err)
|
gui.handleCredentialsPopup(err)
|
||||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleForceCheckout() error {
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
message := gui.Tr.SureForceCheckout
|
message := gui.Tr.SureForceCheckout
|
||||||
title := gui.Tr.ForceCheckoutBranch
|
title := gui.Tr.ForceCheckoutBranch
|
||||||
@@ -135,7 +139,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
|||||||
title: title,
|
title: title,
|
||||||
prompt: message,
|
prompt: message,
|
||||||
handleConfirm: func() error {
|
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)
|
_ = gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||||
@@ -147,6 +151,7 @@ type handleCheckoutRefOptions struct {
|
|||||||
WaitingStatus string
|
WaitingStatus string
|
||||||
EnvVars []string
|
EnvVars []string
|
||||||
onRefNotFound func(ref string) error
|
onRefNotFound func(ref string) error
|
||||||
|
span string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||||
@@ -164,8 +169,10 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
|||||||
gui.State.Panels.Commits.LimitCommits = true
|
gui.State.Panels.Commits.LimitCommits = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitCommand := gui.GitCommand.WithSpan(options.span)
|
||||||
|
|
||||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
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
|
// 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") {
|
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||||
@@ -179,15 +186,15 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
|||||||
title: gui.Tr.AutoStashTitle,
|
title: gui.Tr.AutoStashTitle,
|
||||||
prompt: gui.Tr.AutoStashPrompt,
|
prompt: gui.Tr.AutoStashPrompt,
|
||||||
handleConfirm: func() error {
|
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)
|
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)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
onSuccess()
|
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 {
|
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -208,12 +215,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{
|
return gui.prompt(promptOpts{
|
||||||
title: gui.Tr.BranchName + ":",
|
title: gui.Tr.BranchName + ":",
|
||||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
|
||||||
handleConfirm: func(response string) error {
|
handleConfirm: func(response string) error {
|
||||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||||
|
span: "Checkout branch",
|
||||||
onRefNotFound: func(ref string) error {
|
onRefNotFound: func(ref string) error {
|
||||||
|
|
||||||
return gui.ask(askOpts{
|
return gui.ask(askOpts{
|
||||||
@@ -251,7 +259,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
|
|||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
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)
|
return gui.deleteBranch(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,14 +294,14 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
|||||||
title: title,
|
title: title,
|
||||||
prompt: message,
|
prompt: message,
|
||||||
handleConfirm: func() error {
|
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()
|
errMessage := err.Error()
|
||||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
if !force && strings.Contains(errMessage, "git branch -D ") {
|
||||||
return gui.deleteNamedBranch(selectedBranch, true)
|
return gui.deleteNamedBranch(selectedBranch, true)
|
||||||
}
|
}
|
||||||
return gui.createErrorPanel(errMessage)
|
return gui.createErrorPanel(errMessage)
|
||||||
}
|
}
|
||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -322,13 +330,13 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
|||||||
title: gui.Tr.MergingTitle,
|
title: gui.Tr.MergingTitle,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
handleConfirm: func() error {
|
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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -337,7 +345,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleRebaseOntoLocalBranch() error {
|
||||||
selectedBranchName := gui.getSelectedBranch().Name
|
selectedBranchName := gui.getSelectedBranch().Name
|
||||||
return gui.handleRebaseOntoBranch(selectedBranchName)
|
return gui.handleRebaseOntoBranch(selectedBranchName)
|
||||||
}
|
}
|
||||||
@@ -363,24 +371,22 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
|||||||
title: gui.Tr.RebasingTitle,
|
title: gui.Tr.RebasingTitle,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
err := gui.GitCommand.RebaseBranch(selectedBranchName)
|
err := gui.GitCommand.WithSpan(gui.Tr.Spans.RebaseBranch).RebaseBranch(selectedBranchName)
|
||||||
return gui.handleGenericMergeCommandResult(err)
|
return gui.handleGenericMergeCommandResult(err)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleFastForward() error {
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
if branch == nil {
|
if branch == nil || !branch.IsRealBranch() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if branch.Pushables == "" {
|
|
||||||
return nil
|
if !branch.IsTrackingRemote() {
|
||||||
}
|
|
||||||
if branch.Pushables == "?" {
|
|
||||||
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
|
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
|
||||||
}
|
}
|
||||||
if branch.Pushables != "0" {
|
if branch.HasCommitsToPush() {
|
||||||
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
|
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +395,8 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span := gui.Tr.Spans.FastForwardBranch
|
||||||
|
|
||||||
split := strings.Split(upstream, "/")
|
split := strings.Split(upstream, "/")
|
||||||
remoteName := split[0]
|
remoteName := split[0]
|
||||||
remoteBranchName := strings.Join(split[1:], "/")
|
remoteBranchName := strings.Join(split[1:], "/")
|
||||||
@@ -404,17 +412,17 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
|||||||
_ = gui.createLoaderPanel(message)
|
_ = gui.createLoaderPanel(message)
|
||||||
|
|
||||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
_ = gui.pullWithLock(PullFilesOptions{span: span, FastForwardOnly: true})
|
||||||
} else {
|
} 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.handleCredentialsPopup(err)
|
||||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleCreateResetToBranchMenu() error {
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
if branch == nil {
|
if branch == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -423,30 +431,35 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
|
|||||||
return gui.createResetMenu(branch.Name)
|
return gui.createResetMenu(branch.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleRenameBranch() error {
|
||||||
branch := gui.getSelectedBranch()
|
branch := gui.getSelectedBranch()
|
||||||
if branch == nil {
|
if branch == nil || !branch.IsRealBranch() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
|
|
||||||
// way to get it to show up in the reflog)
|
|
||||||
|
|
||||||
promptForNewName := func() error {
|
promptForNewName := func() error {
|
||||||
return gui.prompt(promptOpts{
|
return gui.prompt(promptOpts{
|
||||||
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
|
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
|
||||||
initialContent: branch.Name,
|
initialContent: branch.Name,
|
||||||
handleConfirm: func(newBranchName string) error {
|
handleConfirm: func(newBranchName string) error {
|
||||||
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
|
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RenameBranch).RenameBranch(branch.Name, newBranchName); err != nil {
|
||||||
return gui.surfaceError(err)
|
|
||||||
}
|
|
||||||
// need to checkout so that the branch shows up in our reflog and therefore
|
|
||||||
// doesn't get lost among all the other branches when we switch to something else
|
|
||||||
if err := gui.GitCommand.Checkout(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
|
|
||||||
return gui.surfaceError(err)
|
return gui.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 +467,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
|
// 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.
|
// but if we've selected it we'll already know that via Pullables and Pullables.
|
||||||
// Bit of a hack but I'm lazy.
|
// Bit of a hack but I'm lazy.
|
||||||
notTrackingRemote := branch.Pullables == "?"
|
if !branch.IsTrackingRemote() {
|
||||||
if notTrackingRemote {
|
|
||||||
return promptForNewName()
|
return promptForNewName()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +486,7 @@ func (gui *Gui) currentBranch() *models.Branch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||||
context := gui.currentSideContext()
|
context := gui.currentSideListContext()
|
||||||
|
|
||||||
item, ok := context.GetSelectedItem()
|
item, ok := context.GetSelectedItem()
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -490,15 +502,15 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
|||||||
|
|
||||||
prefilledName := ""
|
prefilledName := ""
|
||||||
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
||||||
// will set to the remote's existing name
|
// will set to the remote's branch name without the remote name
|
||||||
prefilledName = item.ID()
|
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.prompt(promptOpts{
|
return gui.prompt(promptOpts{
|
||||||
title: message,
|
title: message,
|
||||||
initialContent: prefilledName,
|
initialContent: prefilledName,
|
||||||
handleConfirm: func(response string) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,8 +520,8 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
|||||||
context.GetPanelState().SetSelectedLineIdx(0)
|
context.GetPanelState().SetSelectedLineIdx(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.GetKey() != gui.Contexts.Branches.Context.GetKey() {
|
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
|
||||||
if err := gui.pushContext(gui.Contexts.Branches.Context); err != nil {
|
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,32 +533,6 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getBranchNames() []string {
|
|
||||||
result := make([]string, len(gui.State.Branches))
|
|
||||||
|
|
||||||
for i, branch := range gui.State.Branches {
|
|
||||||
result[i] = branch.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
|
|
||||||
branchNames := gui.getBranchNames()
|
|
||||||
|
|
||||||
matchingBranchNames := utils.FuzzySearch(sanitizedBranchName(input), branchNames)
|
|
||||||
|
|
||||||
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
|
|
||||||
for i, branchName := range matchingBranchNames {
|
|
||||||
suggestions[i] = &types.Suggestion{
|
|
||||||
Value: branchName,
|
|
||||||
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return suggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
||||||
// git's branch naming requirement.
|
// git's branch naming requirement.
|
||||||
func sanitizedBranchName(input string) string {
|
func sanitizedBranchName(input string) string {
|
||||||
|
|||||||
+10
-10
@@ -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
|
// 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 {
|
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
|
||||||
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
|
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
|
||||||
|
|
||||||
if oldContextKey != context.GetKey() {
|
if oldContextKey != context.GetKey() {
|
||||||
// need to reset the cherry picking mode
|
// 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)
|
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0)
|
||||||
|
|
||||||
return gui.rerenderContextViewIfPresent(oldContextKey)
|
return gui.rerenderContextViewIfPresent(oldContextKey)
|
||||||
@@ -24,7 +24,7 @@ func (gui *Gui) handleCopyCommit() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get currently selected commit, add the sha to state.
|
// get currently selected commit, add the sha to state.
|
||||||
context := gui.currentSideContext()
|
context := gui.currentSideListContext()
|
||||||
if context == nil {
|
if context == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func (gui *Gui) handleCopyCommit() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
item, ok := context.SelectedItem()
|
item, ok := context.GetSelectedItem()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) commitsListForContext() []*models.Commit {
|
func (gui *Gui) commitsListForContext() []*models.Commit {
|
||||||
context := gui.currentSideContext()
|
context := gui.currentSideListContext()
|
||||||
if context == nil {
|
if context == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ func (gui *Gui) handleCopyCommitRange() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get currently selected commit, add the sha to state.
|
// get currently selected commit, add the sha to state.
|
||||||
context := gui.currentSideContext()
|
context := gui.currentSideListContext()
|
||||||
if context == nil {
|
if context == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
|||||||
prompt: gui.Tr.SureCherryPick,
|
prompt: gui.Tr.SureCherryPick,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, 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)
|
return gui.handleGenericMergeCommandResult(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -156,7 +156,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) exitCherryPickingMode() 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.ContextKey = ""
|
||||||
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
|
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
|
||||||
@@ -169,7 +169,7 @@ func (gui *Gui) exitCherryPickingMode() error {
|
|||||||
return gui.rerenderContextViewIfPresent(contextKey)
|
return gui.rerenderContextViewIfPresent(contextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
|
||||||
if contextKey == "" {
|
if contextKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if view.Context == contextKey {
|
if ContextKey(view.Context) == contextKey {
|
||||||
if err := context.HandleRender(); err != nil {
|
if err := context.HandleRender(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 hesitate 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]
|
||||||
|
}
|
||||||
+128
-46
@@ -1,34 +1,51 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"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
|
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 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 {
|
func (gui *Gui) handleCommitFileSelect() error {
|
||||||
gui.escapeLineByLinePanel()
|
gui.escapeLineByLinePanel()
|
||||||
|
|
||||||
commitFile := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if commitFile == nil {
|
if node == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
to := commitFile.Parent
|
to := gui.State.CommitFileManager.GetParent()
|
||||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||||
|
|
||||||
cmd := gui.OSCommand.ExecutableFromString(
|
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{
|
return gui.refreshMainViews(refreshMainOpts{
|
||||||
main: &viewUpdateOpts{
|
main: &viewUpdateOpts{
|
||||||
@@ -39,32 +56,32 @@ func (gui *Gui) handleCommitFileSelect() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleCheckoutCommitFile() error {
|
||||||
file := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if file == nil {
|
if node == nil {
|
||||||
return 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.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
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 {
|
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
|
fileName := gui.getSelectedCommitFileName()
|
||||||
|
|
||||||
return gui.ask(askOpts{
|
return gui.ask(askOpts{
|
||||||
title: gui.Tr.DiscardFileChangesTitle,
|
title: gui.Tr.DiscardFileChangesTitle,
|
||||||
prompt: gui.Tr.DiscardFileChangesPrompt,
|
prompt: gui.Tr.DiscardFileChangesPrompt,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, 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 {
|
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -77,43 +94,50 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshCommitFilesView() error {
|
func (gui *Gui) refreshCommitFilesView() error {
|
||||||
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
|
currentSideContext := gui.currentSideContext()
|
||||||
return err
|
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
|
to := gui.State.Panels.CommitFiles.refName
|
||||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
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 {
|
if err != nil {
|
||||||
return gui.surfaceError(err)
|
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 {
|
func (gui *Gui) handleOpenOldCommitFile() error {
|
||||||
file := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if file == nil {
|
if node == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return gui.openFile(file.Name)
|
return gui.openFile(node.GetPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleEditCommitFile() error {
|
||||||
file := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if file == nil {
|
if node == nil {
|
||||||
return 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 {
|
func (gui *Gui) handleToggleFileForPatch() error {
|
||||||
commitFile := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if commitFile == nil {
|
if node == nil {
|
||||||
return 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 {
|
// if there is any file that hasn't been fully added we'll fully add everything,
|
||||||
return err
|
// 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() {
|
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||||
gui.GitCommand.PatchManager.Reset()
|
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{
|
return gui.ask(askOpts{
|
||||||
title: gui.Tr.DiscardPatch,
|
title: gui.Tr.DiscardPatch,
|
||||||
prompt: gui.Tr.DiscardPatchConfirm,
|
prompt: gui.Tr.DiscardPatchConfirm,
|
||||||
@@ -159,16 +197,20 @@ func (gui *Gui) startPatchManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleEnterCommitFile() error {
|
||||||
return gui.enterCommitFile(-1)
|
return gui.enterCommitFile(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||||
commitFile := gui.getSelectedCommitFile()
|
node := gui.getSelectedCommitFileNode()
|
||||||
if commitFile == nil {
|
if node == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if node.File == nil {
|
||||||
|
return gui.handleToggleCommitFileDirCollapsed()
|
||||||
|
}
|
||||||
|
|
||||||
enterTheFile := func(selectedLineIdx int) error {
|
enterTheFile := func(selectedLineIdx int) error {
|
||||||
if !gui.GitCommand.PatchManager.Active() {
|
if !gui.GitCommand.PatchManager.Active() {
|
||||||
if err := gui.startPatchManager(); err != nil {
|
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 err
|
||||||
}
|
}
|
||||||
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
|
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{
|
return gui.ask(askOpts{
|
||||||
title: gui.Tr.DiscardPatch,
|
title: gui.Tr.DiscardPatch,
|
||||||
prompt: gui.Tr.DiscardPatchConfirm,
|
prompt: gui.Tr.DiscardPatchConfirm,
|
||||||
@@ -192,7 +234,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
|||||||
return enterTheFile(selectedLineIdx)
|
return enterTheFile(selectedLineIdx)
|
||||||
},
|
},
|
||||||
handleClose: func() error {
|
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)
|
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 {
|
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
|
// 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.
|
// 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.SelectedLineIdx = 0
|
||||||
gui.State.Panels.CommitFiles.refName = refName
|
gui.State.Panels.CommitFiles.refName = refName
|
||||||
gui.State.Panels.CommitFiles.canRebase = canRebase
|
gui.State.Panels.CommitFiles.canRebase = canRebase
|
||||||
gui.Contexts.CommitFiles.Context.SetParentContext(context)
|
gui.State.Contexts.CommitFiles.SetParentContext(context)
|
||||||
gui.Contexts.CommitFiles.Context.SetWindowName(windowName)
|
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
|
||||||
|
|
||||||
if err := gui.refreshCommitFilesView(); err != nil {
|
if err := gui.refreshCommitFilesView(); err != nil {
|
||||||
return err
|
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
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
func (gui *Gui) handleCommitConfirm() error {
|
||||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
|
||||||
// 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)
|
|
||||||
if message == "" {
|
if message == "" {
|
||||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
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) {
|
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||||
flags = "--no-verify"
|
flags = "--no-verify"
|
||||||
}
|
}
|
||||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.clearEditorView(v)
|
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||||
|
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||||
_ = gui.returnFromContext()
|
_ = gui.returnFromContext()
|
||||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||||
|
gui.Views.CommitMessage.ClearTextArea()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleCommitClose() error {
|
||||||
return gui.returnFromContext()
|
return gui.returnFromContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,18 +37,18 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
|||||||
message := utils.ResolvePlaceholderString(
|
message := utils.ResolvePlaceholderString(
|
||||||
gui.Tr.CommitMessageConfirm,
|
gui.Tr.CommitMessageConfirm,
|
||||||
map[string]string{
|
map[string]string{
|
||||||
"keyBindClose": "esc",
|
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
|
||||||
"keyBindConfirm": "enter",
|
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
|
||||||
"keyBindNewLine": "tab",
|
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
gui.renderString("options", message)
|
gui.renderString(gui.Views.Options, message)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||||
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
|
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderCommitLength is a function.
|
// RenderCommitLength is a function.
|
||||||
@@ -76,6 +56,6 @@ func (gui *Gui) RenderCommitLength() {
|
|||||||
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
|
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v := gui.getCommitMessageView()
|
|
||||||
v.Subtitle = gui.getBufferLength(v)
|
gui.Views.CommitMessage.Subtitle = gui.getBufferLength(gui.Views.CommitMessage)
|
||||||
}
|
}
|
||||||
|
|||||||
+205
-55
@@ -1,19 +1,23 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// after selecting the 200th commit, we'll load in all the rest
|
||||||
|
const COMMIT_THRESHOLD = 200
|
||||||
|
|
||||||
// list panel functions
|
// list panel functions
|
||||||
|
|
||||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||||
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
|
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
|
||||||
if selectedLine == -1 {
|
if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
|||||||
|
|
||||||
func (gui *Gui) handleCommitSelect() error {
|
func (gui *Gui) handleCommitSelect() error {
|
||||||
state := gui.State.Panels.Commits
|
state := gui.State.Panels.Commits
|
||||||
if state.SelectedLineIdx > 290 && state.LimitCommits {
|
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
|
||||||
state.LimitCommits = false
|
state.LimitCommits = false
|
||||||
go utils.Safe(func() {
|
go utils.Safe(func() {
|
||||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||||
@@ -36,12 +40,12 @@ func (gui *Gui) handleCommitSelect() error {
|
|||||||
var task updateTask
|
var task updateTask
|
||||||
commit := gui.getSelectedLocalCommit()
|
commit := gui.getSelectedLocalCommit()
|
||||||
if commit == nil {
|
if commit == nil {
|
||||||
task = gui.createRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||||
} else {
|
} else {
|
||||||
cmd := gui.OSCommand.ExecutableFromString(
|
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{
|
return gui.refreshMainViews(refreshMainOpts{
|
||||||
@@ -87,7 +91,7 @@ func (gui *Gui) refreshCommits() error {
|
|||||||
|
|
||||||
go utils.Safe(func() {
|
go utils.Safe(func() {
|
||||||
_ = gui.refreshCommitsWithLimit()
|
_ = gui.refreshCommitsWithLimit()
|
||||||
context, ok := gui.Contexts.CommitFiles.Context.GetParentContext()
|
context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
|
||||||
if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
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.
|
// 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
|
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
|
||||||
@@ -118,9 +122,10 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
|||||||
commits, err := builder.GetCommits(
|
commits, err := builder.GetCommits(
|
||||||
commands.GetCommitsOptions{
|
commands.GetCommitsOptions{
|
||||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||||
FilterPath: gui.State.Modes.Filtering.Path,
|
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||||
IncludeRebaseCommits: true,
|
IncludeRebaseCommits: true,
|
||||||
RefName: "HEAD",
|
RefName: "HEAD",
|
||||||
|
All: gui.State.ShowWholeGitGraph,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -128,7 +133,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
|||||||
}
|
}
|
||||||
gui.State.Commits = commits
|
gui.State.Commits = commits
|
||||||
|
|
||||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshRebaseCommits() error {
|
func (gui *Gui) refreshRebaseCommits() error {
|
||||||
@@ -143,12 +148,12 @@ func (gui *Gui) refreshRebaseCommits() error {
|
|||||||
}
|
}
|
||||||
gui.State.Commits = updatedCommits
|
gui.State.Commits = updatedCommits
|
||||||
|
|
||||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||||
}
|
}
|
||||||
|
|
||||||
// specific functions
|
// 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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -170,14 +175,14 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
|||||||
prompt: gui.Tr.SureSquashThisCommit,
|
prompt: gui.Tr.SureSquashThisCommit,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, 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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -199,14 +204,14 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
|||||||
prompt: gui.Tr.SureFixupThisCommit,
|
prompt: gui.Tr.SureFixupThisCommit,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.FixingStatus, 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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -237,7 +242,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
title: gui.Tr.LcRenameCommit,
|
title: gui.Tr.LcRenameCommit,
|
||||||
initialContent: message,
|
initialContent: message,
|
||||||
handleConfirm: func(response string) error {
|
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)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +251,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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -259,13 +264,12 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
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 {
|
if err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
if subProcess != nil {
|
if subProcess != nil {
|
||||||
gui.SubProcess = subProcess
|
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
|
||||||
return gui.Errors.ErrSubProcess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -288,6 +292,12 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
|||||||
return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
|
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 {
|
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
|
||||||
return false, gui.surfaceError(err)
|
return false, gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
@@ -295,7 +305,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
|||||||
return true, gui.refreshRebaseCommits()
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -313,24 +323,35 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
|||||||
prompt: gui.Tr.DeleteCommitPrompt,
|
prompt: gui.Tr.DeleteCommitPrompt,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, 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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span := gui.Tr.Spans.MoveCommitDown
|
||||||
|
|
||||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||||
selectedCommit := gui.State.Commits[index]
|
selectedCommit := gui.State.Commits[index]
|
||||||
if selectedCommit.Status == "rebasing" {
|
if selectedCommit.Status == "rebasing" {
|
||||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||||
return nil
|
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 {
|
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
@@ -339,7 +360,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() 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 {
|
if err == nil {
|
||||||
gui.State.Panels.Commits.SelectedLineIdx++
|
gui.State.Panels.Commits.SelectedLineIdx++
|
||||||
}
|
}
|
||||||
@@ -347,7 +368,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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,8 +377,19 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if index == 0 {
|
if index == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span := gui.Tr.Spans.MoveCommitUp
|
||||||
|
|
||||||
selectedCommit := gui.State.Commits[index]
|
selectedCommit := gui.State.Commits[index]
|
||||||
if selectedCommit.Status == "rebasing" {
|
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 {
|
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
@@ -366,7 +398,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() 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 {
|
if err == nil {
|
||||||
gui.State.Panels.Commits.SelectedLineIdx--
|
gui.State.Panels.Commits.SelectedLineIdx--
|
||||||
}
|
}
|
||||||
@@ -374,7 +406,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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -388,12 +420,12 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() 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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -403,14 +435,14 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
|||||||
prompt: gui.Tr.AmendCommitPrompt,
|
prompt: gui.Tr.AmendCommitPrompt,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, 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)
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -425,19 +457,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
|
// 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
|
// 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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
commit := gui.getSelectedLocalCommit()
|
||||||
return gui.surfaceError(err)
|
|
||||||
|
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++
|
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 {
|
func (gui *Gui) handleViewCommitFiles() error {
|
||||||
@@ -446,10 +512,10 @@ func (gui *Gui) handleViewCommitFiles() error {
|
|||||||
return nil
|
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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -470,7 +536,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
title: gui.Tr.CreateFixupCommit,
|
title: gui.Tr.CreateFixupCommit,
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
handleConfirm: func() error {
|
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)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +545,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 {
|
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -501,14 +567,14 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
|
|||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
handleConfirm: func() error {
|
handleConfirm: func() error {
|
||||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, 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)
|
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
|
// 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
|
// if annotated, switch to a subprocess to create the message
|
||||||
|
|
||||||
@@ -524,15 +590,15 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
|||||||
return gui.prompt(promptOpts{
|
return gui.prompt(promptOpts{
|
||||||
title: gui.Tr.TagNameTitle,
|
title: gui.Tr.TagNameTitle,
|
||||||
handleConfirm: func(response string) error {
|
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.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()
|
commit := gui.getSelectedLocalCommit()
|
||||||
if commit == nil {
|
if commit == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -542,12 +608,12 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
|||||||
title: gui.Tr.LcCheckoutCommit,
|
title: gui.Tr.LcCheckoutCommit,
|
||||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||||
handleConfirm: func() error {
|
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()
|
commit := gui.getSelectedLocalCommit()
|
||||||
if commit == nil {
|
if commit == nil {
|
||||||
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
|
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
|
||||||
@@ -556,30 +622,30 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return gui.createResetMenu(commit.Sha)
|
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
|
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||||
if gui.State.Panels.Commits.LimitCommits {
|
if gui.State.Panels.Commits.LimitCommits {
|
||||||
gui.State.Panels.Commits.LimitCommits = false
|
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 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
|
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||||
if gui.State.Panels.Commits.LimitCommits {
|
if gui.State.Panels.Commits.LimitCommits {
|
||||||
gui.State.Panels.Commits.LimitCommits = false
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, context := range gui.getListContexts() {
|
for _, context := range gui.getListContexts() {
|
||||||
if context.ViewName == "commits" {
|
if context.GetViewName() == "commits" {
|
||||||
return context.handleGotoBottom(g, v)
|
return context.handleGotoBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,7 +663,7 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
|||||||
return gui.surfaceError(err)
|
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)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,3 +671,87 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleOpenLogMenu() error {
|
||||||
|
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||||
|
{
|
||||||
|
displayString: gui.Tr.ToggleShowGitGraphAll,
|
||||||
|
onPress: func() error {
|
||||||
|
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
|
||||||
|
|
||||||
|
if gui.State.ShowWholeGitGraph {
|
||||||
|
gui.State.Panels.Commits.LimitCommits = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||||
|
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: gui.Tr.ShowGitGraph,
|
||||||
|
opensMenu: true,
|
||||||
|
onPress: func() error {
|
||||||
|
onSelect := func(value string) {
|
||||||
|
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
|
||||||
|
gui.render()
|
||||||
|
}
|
||||||
|
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||||
|
{
|
||||||
|
displayString: "always",
|
||||||
|
onPress: func() error {
|
||||||
|
onSelect("always")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: "never",
|
||||||
|
onPress: func() error {
|
||||||
|
onSelect("never")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: "when maximised",
|
||||||
|
onPress: func() error {
|
||||||
|
onSelect("when-maximised")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, createMenuOptions{showCancel: true})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: gui.Tr.SortCommits,
|
||||||
|
opensMenu: true,
|
||||||
|
onPress: func() error {
|
||||||
|
onSelect := func(value string) error {
|
||||||
|
gui.Config.GetUserConfig().Git.Log.Order = value
|
||||||
|
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||||
|
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||||
|
{
|
||||||
|
displayString: "topological (topo-order)",
|
||||||
|
onPress: func() error {
|
||||||
|
return onSelect("topo-order")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: "date-order",
|
||||||
|
onPress: func() error {
|
||||||
|
return onSelect("date-order")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayString: "author-date-order",
|
||||||
|
onPress: func() error {
|
||||||
|
return onSelect("author-date-order")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, createMenuOptions{showCancel: true})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, createMenuOptions{showCancel: true})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
// lots of this has been directly ported from one of the example files, will brush up later
|
// lots of this has been directly ported from one of the example files, will brush up later
|
||||||
|
|
||||||
// Copyright 2014 The gocui Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
@@ -38,7 +34,6 @@ type askOpts struct {
|
|||||||
handleConfirm func() error
|
handleConfirm func() error
|
||||||
handleClose func() error
|
handleClose func() error
|
||||||
handlersManageFocus bool
|
handlersManageFocus bool
|
||||||
findSuggestionsFunc func(string) []*types.Suggestion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type promptOpts struct {
|
type promptOpts struct {
|
||||||
@@ -55,7 +50,6 @@ func (gui *Gui) ask(opts askOpts) error {
|
|||||||
handleConfirm: opts.handleConfirm,
|
handleConfirm: opts.handleConfirm,
|
||||||
handleClose: opts.handleClose,
|
handleClose: opts.handleClose,
|
||||||
handlersManageFocus: opts.handlersManageFocus,
|
handlersManageFocus: opts.handlersManageFocus,
|
||||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,19 +102,10 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) deleteConfirmationView() {
|
|
||||||
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 {
|
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||||
view := gui.getConfirmationView()
|
// we've already closed it so we can just return
|
||||||
if view == nil {
|
if !gui.Views.Confirmation.Visible {
|
||||||
return nil // if it's already been closed we can just return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handlersManageFocus {
|
if !handlersManageFocus {
|
||||||
@@ -129,9 +114,9 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.deleteConfirmationView()
|
gui.clearConfirmationViewKeyBindings()
|
||||||
|
gui.Views.Confirmation.Visible = false
|
||||||
_, _ = gui.g.SetViewOnBottom("suggestions")
|
gui.Views.Suggestions.Visible = false
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -172,67 +157,78 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
|
|||||||
height/2 + panelHeight/2
|
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,
|
||||||
|
editable bool,
|
||||||
|
) error {
|
||||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
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 != nil {
|
||||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
confirmationView.HasLoader = hasLoader
|
|
||||||
if hasLoader {
|
|
||||||
gui.g.StartTicking()
|
|
||||||
}
|
|
||||||
confirmationView.Title = title
|
|
||||||
confirmationView.Wrap = true
|
|
||||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
|
||||||
}
|
}
|
||||||
|
gui.Views.Confirmation.HasLoader = hasLoader
|
||||||
|
if hasLoader {
|
||||||
|
gui.g.StartTicking()
|
||||||
|
}
|
||||||
|
gui.Views.Confirmation.Title = title
|
||||||
|
// for now we do not support wrapping in our editor
|
||||||
|
gui.Views.Confirmation.Wrap = !editable
|
||||||
|
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
||||||
|
|
||||||
gui.findSuggestions = findSuggestionsFunc
|
gui.findSuggestions = findSuggestionsFunc
|
||||||
if findSuggestionsFunc != nil {
|
if findSuggestionsFunc != nil {
|
||||||
suggestionsViewHeight := 11
|
suggestionsViewHeight := 11
|
||||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
|
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
suggestionsView.Wrap = true
|
|
||||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
|
||||||
}
|
}
|
||||||
gui.setSuggestions([]*types.Suggestion{})
|
suggestionsView.Wrap = false
|
||||||
_, _ = gui.g.SetViewOnTop("suggestions")
|
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||||
|
gui.setSuggestions(findSuggestionsFunc(""))
|
||||||
|
suggestionsView.Visible = true
|
||||||
|
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
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 {
|
func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
// delete the existing confirmation panel if it exists
|
// remove any previous keybindings
|
||||||
if view, _ := g.View("confirmation"); view != nil {
|
gui.clearConfirmationViewKeyBindings()
|
||||||
gui.deleteConfirmationView()
|
|
||||||
}
|
err := gui.prepareConfirmationPanel(
|
||||||
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
opts.title,
|
||||||
|
opts.prompt,
|
||||||
|
opts.hasLoader,
|
||||||
|
opts.findSuggestionsFunc,
|
||||||
|
opts.editable,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
confirmationView := gui.Views.Confirmation
|
||||||
confirmationView.Editable = opts.editable
|
confirmationView.Editable = opts.editable
|
||||||
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
|
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.renderString("confirmation", opts.prompt)
|
if opts.editable {
|
||||||
|
textArea := confirmationView.TextArea
|
||||||
|
textArea.Clear()
|
||||||
|
textArea.TypeString(opts.prompt)
|
||||||
|
confirmationView.RenderTextArea()
|
||||||
|
} else {
|
||||||
|
if err := gui.renderStringSync(confirmationView, opts.prompt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return gui.setKeyBindings(opts)
|
return gui.setKeyBindings(opts)
|
||||||
})
|
})
|
||||||
@@ -248,10 +244,10 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
gui.renderString("options", actions)
|
gui.renderString(gui.Views.Options, actions)
|
||||||
var onConfirm func() error
|
var onConfirm func() error
|
||||||
if opts.handleConfirmPrompt != nil {
|
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.TextArea.GetContent() })
|
||||||
} else {
|
} else {
|
||||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||||
}
|
}
|
||||||
@@ -263,7 +259,11 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
|
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
|
||||||
|
opts.handlersManageFocus,
|
||||||
|
opts.handleConfirmPrompt,
|
||||||
|
gui.getSelectedSuggestionValue,
|
||||||
|
)
|
||||||
|
|
||||||
confirmationKeybindings := []confirmationKeybinding{
|
confirmationKeybindings := []confirmationKeybinding{
|
||||||
{
|
{
|
||||||
@@ -284,7 +284,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
|||||||
{
|
{
|
||||||
viewName: "confirmation",
|
viewName: "confirmation",
|
||||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||||
handler: func() error { return gui.replaceContext(gui.Contexts.Suggestions.Context) },
|
handler: func() error {
|
||||||
|
if len(gui.State.Suggestions) > 0 {
|
||||||
|
return gui.replaceContext(gui.State.Contexts.Suggestions)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewName: "suggestions",
|
viewName: "suggestions",
|
||||||
@@ -304,7 +309,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
|||||||
{
|
{
|
||||||
viewName: "suggestions",
|
viewName: "suggestions",
|
||||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
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 +322,24 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||||
|
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||||
|
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||||
|
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||||
|
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||||
|
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||||
|
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||||
|
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) createErrorPanel(message string) error {
|
func (gui *Gui) createErrorPanel(message string) error {
|
||||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
|
||||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
|
||||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -335,11 +355,5 @@ func (gui *Gui) surfaceError(err error) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sentinelError := range gui.sentinelErrorsArr() {
|
|
||||||
if err == sentinelError {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.createErrorPanel(err.Error())
|
return gui.createErrorPanel(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
+199
-467
@@ -6,131 +6,25 @@ import (
|
|||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ContextKind int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SIDE_CONTEXT int = iota
|
SIDE_CONTEXT ContextKind = iota
|
||||||
MAIN_CONTEXT
|
MAIN_CONTEXT
|
||||||
TEMPORARY_POPUP
|
TEMPORARY_POPUP
|
||||||
PERSISTENT_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 {
|
type Context interface {
|
||||||
HandleFocus() error
|
HandleFocus() error
|
||||||
HandleFocusLost() error
|
HandleFocusLost() error
|
||||||
HandleRender() error
|
HandleRender() error
|
||||||
GetKind() int
|
GetKind() ContextKind
|
||||||
GetViewName() string
|
GetViewName() string
|
||||||
GetWindowName() string
|
GetWindowName() string
|
||||||
SetWindowName(string)
|
SetWindowName(string)
|
||||||
GetKey() string
|
GetKey() ContextKey
|
||||||
SetParentContext(Context)
|
SetParentContext(Context)
|
||||||
|
|
||||||
// we return a bool here to tell us whether or not the returned value just wraps a nil
|
// 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
|
GetOptionsMap() map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicContext struct {
|
func (gui *Gui) popupViewNames() []string {
|
||||||
OnFocus func() error
|
result := []string{}
|
||||||
OnFocusLost func() error
|
for _, context := range gui.allContexts() {
|
||||||
OnRender func() error
|
if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
|
||||||
OnGetOptionsMap func() map[string]string
|
result = append(result, context.GetViewName())
|
||||||
Kind int
|
}
|
||||||
Key string
|
|
||||||
ViewName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c BasicContext) GetOptionsMap() map[string]string {
|
|
||||||
if c.OnGetOptionsMap != nil {
|
|
||||||
return c.OnGetOptionsMap()
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c BasicContext) SetWindowName(windowName string) {
|
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
|
||||||
panic("can't set window name on basic context")
|
gui.State.ContextManager.RLock()
|
||||||
}
|
defer gui.State.ContextManager.RUnlock()
|
||||||
|
|
||||||
func (c BasicContext) GetWindowName() string {
|
stack := gui.State.ContextManager.ContextStack
|
||||||
// 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
|
|
||||||
|
|
||||||
for i := range stack {
|
for i := range stack {
|
||||||
reversedIndex := len(stack) - 1 - i
|
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.
|
// hitting escape: you want to go that context's parent instead.
|
||||||
func (gui *Gui) replaceContext(c Context) error {
|
func (gui *Gui) replaceContext(c Context) error {
|
||||||
gui.g.Update(func(*gocui.Gui) error {
|
gui.g.Update(func(*gocui.Gui) error {
|
||||||
if len(gui.State.ContextStack) == 0 {
|
gui.State.ContextManager.Lock()
|
||||||
gui.State.ContextStack = []Context{c}
|
defer gui.State.ContextManager.Unlock()
|
||||||
|
|
||||||
|
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||||
|
gui.State.ContextManager.ContextStack = []Context{c}
|
||||||
} else {
|
} else {
|
||||||
// replace the last item with the given item
|
// 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)
|
return gui.activateContext(c)
|
||||||
@@ -426,28 +83,42 @@ func (gui *Gui) replaceContext(c Context) error {
|
|||||||
|
|
||||||
func (gui *Gui) pushContext(c Context) error {
|
func (gui *Gui) pushContext(c Context) error {
|
||||||
gui.g.Update(func(*gocui.Gui) error {
|
gui.g.Update(func(*gocui.Gui) error {
|
||||||
// push onto stack
|
return gui.pushContextDirect(c)
|
||||||
// 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 nil
|
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
|
// 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
|
// 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
|
// look up the context currently active for that view and switch to that context
|
||||||
@@ -457,34 +128,49 @@ func (gui *Gui) pushContextWithView(viewName string) error {
|
|||||||
|
|
||||||
func (gui *Gui) returnFromContext() error {
|
func (gui *Gui) returnFromContext() error {
|
||||||
gui.g.Update(func(*gocui.Gui) error {
|
gui.g.Update(func(*gocui.Gui) error {
|
||||||
// TODO: add mutexes
|
return gui.returnFromContextSync()
|
||||||
|
|
||||||
if len(gui.State.ContextStack) == 1 {
|
|
||||||
// cannot escape from bottommost context
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
n := len(gui.State.ContextStack) - 1
|
|
||||||
|
|
||||||
currentContext := gui.State.ContextStack[n]
|
|
||||||
newContext := gui.State.ContextStack[n-1]
|
|
||||||
|
|
||||||
gui.State.ContextStack = gui.State.ContextStack[:n]
|
|
||||||
|
|
||||||
if err := gui.deactivateContext(currentContext); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gui.activateContext(newContext)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) returnFromContextSync() error {
|
||||||
|
gui.State.ContextManager.Lock()
|
||||||
|
|
||||||
|
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||||
|
// cannot escape from bottommost context
|
||||||
|
gui.State.ContextManager.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||||
|
|
||||||
|
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||||
|
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||||
|
|
||||||
|
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||||
|
|
||||||
|
gui.State.ContextManager.Unlock()
|
||||||
|
|
||||||
|
if err := gui.deactivateContext(currentContext); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.activateContext(newContext)
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) deactivateContext(c Context) error {
|
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 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 {
|
if view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
|
||||||
_, _ = gui.g.SetViewOnBottom(c.GetViewName())
|
view.Visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.HandleFocusLost(); err != nil {
|
if err := c.HandleFocusLost(); err != nil {
|
||||||
@@ -503,7 +189,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Context != c.GetKey() {
|
if ContextKey(v.Context) != c.GetKey() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,14 +209,13 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
|||||||
func (gui *Gui) activateContext(c Context) error {
|
func (gui *Gui) activateContext(c Context) error {
|
||||||
viewName := c.GetViewName()
|
viewName := c.GetViewName()
|
||||||
v, err := gui.g.View(viewName)
|
v, err := gui.g.View(viewName)
|
||||||
// if view no longer exists, pop again
|
|
||||||
if err != nil {
|
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.
|
// 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" {
|
if viewName == "main" {
|
||||||
gui.changeMainViewsContext(c.GetKey())
|
gui.changeMainViewsContext(c.GetKey())
|
||||||
@@ -541,14 +226,10 @@ func (gui *Gui) activateContext(c Context) error {
|
|||||||
gui.setViewTabForContext(c)
|
gui.setViewTabForContext(c)
|
||||||
|
|
||||||
if _, err := gui.g.SetCurrentView(viewName); err != nil {
|
if _, err := gui.g.SetCurrentView(viewName); err != nil {
|
||||||
// if view no longer exists, pop again
|
return err
|
||||||
return gui.returnFromContext()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
|
v.Visible = true
|
||||||
// if view no longer exists, pop again
|
|
||||||
return gui.returnFromContext()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the new context's view was previously displaying another context, render the new context
|
// if the new context's view was previously displaying another context, render the new context
|
||||||
if originalViewContextKey != c.GetKey() {
|
if originalViewContextKey != c.GetKey() {
|
||||||
@@ -557,7 +238,7 @@ func (gui *Gui) activateContext(c Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.Context = c.GetKey()
|
v.Context = string(c.GetKey())
|
||||||
|
|
||||||
gui.g.Cursor = v.Editable
|
gui.g.Cursor = v.Editable
|
||||||
|
|
||||||
@@ -578,29 +259,50 @@ func (gui *Gui) activateContext(c Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// currently unused
|
// // currently unused
|
||||||
// func (gui *Gui) renderContextStack() string {
|
// func (gui *Gui) renderContextStack() string {
|
||||||
// result := ""
|
// result := ""
|
||||||
// for _, context := range gui.State.ContextStack {
|
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||||
// result += context.GetKey() + "\n"
|
// result += string(context.GetKey()) + "\n"
|
||||||
// }
|
// }
|
||||||
// return result
|
// return result
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (gui *Gui) currentContext() Context {
|
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.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 {
|
// the status panel is not yet a list context (and may never be), so this method is not
|
||||||
stack := gui.State.ContextStack
|
// quite the same as currentSideContext()
|
||||||
|
func (gui *Gui) currentSideListContext() IListContext {
|
||||||
|
context := gui.currentSideContext()
|
||||||
|
listContext, ok := context.(IListContext)
|
||||||
|
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
|
// on startup the stack can be empty so we'll return an empty string in that case
|
||||||
if len(stack) == 0 {
|
if len(stack) == 0 {
|
||||||
return nil
|
return gui.defaultSideContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the first context in the stack with the type of SIDE_CONTEXT
|
// find the first context in the stack with the type of SIDE_CONTEXT
|
||||||
@@ -608,22 +310,45 @@ func (gui *Gui) currentSideContext() *ListContext {
|
|||||||
context := stack[len(stack)-1-i]
|
context := stack[len(stack)-1-i]
|
||||||
|
|
||||||
if context.GetKind() == SIDE_CONTEXT {
|
if context.GetKind() == SIDE_CONTEXT {
|
||||||
// not all side contexts are list contexts (e.g. the status panel)
|
return context
|
||||||
listContext, ok := context.(*ListContext)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return listContext
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
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() {
|
func (gui *Gui) setInitialViewContexts() {
|
||||||
// arguably we should only have our ViewContextMap and we should do away with
|
// arguably we should only have our ViewContextMap and we should do away with
|
||||||
// contexts on views, or vice versa
|
// contexts on views, or vice versa
|
||||||
@@ -634,7 +359,7 @@ func (gui *Gui) setInitialViewContexts() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
view.Context = context.GetKey()
|
view.Context = string(context.GetKey())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,18 +389,21 @@ func (gui *Gui) onViewFocusChange() error {
|
|||||||
|
|
||||||
currentView := gui.g.CurrentView()
|
currentView := gui.g.CurrentView()
|
||||||
for _, view := range gui.g.Views() {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error {
|
||||||
if v == nil {
|
if oldView == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.IsSearching() && newView.Name() != "search" {
|
_ = oldView.SetOriginX(0)
|
||||||
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -687,15 +415,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
|
// 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
|
// other views can have their context changed directly but this function helps
|
||||||
// keep the main and secondary views in sync
|
// 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 {
|
if gui.State.MainContext == contextKey {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch contextKey {
|
switch contextKey {
|
||||||
case MAIN_NORMAL_CONTEXT_KEY, MAIN_PATCH_BUILDING_CONTEXT_KEY, MAIN_STAGING_CONTEXT_KEY, MAIN_MERGING_CONTEXT_KEY:
|
case MAIN_NORMAL_CONTEXT_KEY, MAIN_PATCH_BUILDING_CONTEXT_KEY, MAIN_STAGING_CONTEXT_KEY, MAIN_MERGING_CONTEXT_KEY:
|
||||||
gui.getMainView().Context = contextKey
|
gui.Views.Main.Context = string(contextKey)
|
||||||
gui.getSecondaryView().Context = contextKey
|
gui.Views.Secondary.Context = string(contextKey)
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
|
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
|
||||||
}
|
}
|
||||||
@@ -704,7 +432,7 @@ func (gui *Gui) changeMainViewsContext(contextKey string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) viewTabNames(viewName string) []string {
|
func (gui *Gui) viewTabNames(viewName string) []string {
|
||||||
tabContexts := gui.ViewTabContextMap[viewName]
|
tabContexts := gui.State.ViewTabContextMap[viewName]
|
||||||
|
|
||||||
if len(tabContexts) == 0 {
|
if len(tabContexts) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -720,7 +448,7 @@ func (gui *Gui) viewTabNames(viewName string) []string {
|
|||||||
|
|
||||||
func (gui *Gui) setViewTabForContext(c Context) {
|
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
|
// 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 {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -746,7 +474,7 @@ type tabContext struct {
|
|||||||
contexts []Context
|
contexts []Context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
|
||||||
context, ok := gui.contextForContextKey(contextKey)
|
context, ok := gui.contextForContextKey(contextKey)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -756,7 +484,7 @@ func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
|
||||||
for _, context := range gui.allContexts() {
|
for _, context := range gui.allContexts() {
|
||||||
if context.GetKey() == contextKey {
|
if context.GetKey() == contextKey {
|
||||||
return context, true
|
return context, true
|
||||||
@@ -766,18 +494,28 @@ func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) rerenderView(viewName string) error {
|
func (gui *Gui) rerenderView(view *gocui.View) error {
|
||||||
v, err := gui.g.View(viewName)
|
contextKey := ContextKey(view.Context)
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
contextKey := v.Context
|
|
||||||
context := gui.mustContextForContextKey(contextKey)
|
context := gui.mustContextForContextKey(contextKey)
|
||||||
|
|
||||||
return context.HandleRender()
|
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
|
// currently unused
|
||||||
// func (gui *Gui) getCurrentSideView() *gocui.View {
|
// func (gui *Gui) getCurrentSideView() *gocui.View {
|
||||||
// currentSideContext := gui.currentSideContext()
|
// currentSideContext := gui.currentSideContext()
|
||||||
@@ -790,17 +528,11 @@ func (gui *Gui) rerenderView(viewName string) error {
|
|||||||
// return view
|
// return view
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (gui *Gui) getSideContextSelectedItemId() string {
|
// currently unused
|
||||||
currentSideContext := gui.currentSideContext()
|
// func (gui *Gui) renderContextStack() string {
|
||||||
if currentSideContext == nil {
|
// result := ""
|
||||||
return ""
|
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||||
}
|
// result += context.GetViewName() + "\n"
|
||||||
|
// }
|
||||||
item, ok := currentSideContext.GetSelectedItem()
|
// return result
|
||||||
|
// }
|
||||||
if ok {
|
|
||||||
return item.ID()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 IListContext
|
||||||
|
Submodules IListContext
|
||||||
|
Menu IListContext
|
||||||
|
Branches IListContext
|
||||||
|
Remotes IListContext
|
||||||
|
RemoteBranches IListContext
|
||||||
|
Tags IListContext
|
||||||
|
BranchCommits IListContext
|
||||||
|
CommitFiles IListContext
|
||||||
|
ReflogCommits IListContext
|
||||||
|
SubCommits IListContext
|
||||||
|
Stash IListContext
|
||||||
|
Suggestions IListContext
|
||||||
|
Normal Context
|
||||||
|
Staging Context
|
||||||
|
PatchBuilding Context
|
||||||
|
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 {
|
func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||||
gui.credentials = make(chan string)
|
gui.credentials = make(chan string)
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
credentialsView, _ := g.View("credentials")
|
credentialsView := gui.Views.Credentials
|
||||||
switch passOrUname {
|
switch passOrUname {
|
||||||
case "username":
|
case "username":
|
||||||
credentialsView.Title = gui.Tr.CredentialsUsername
|
credentialsView.Title = gui.Tr.CredentialsUsername
|
||||||
@@ -26,7 +26,7 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
|||||||
credentialsView.Mask = '*'
|
credentialsView.Mask = '*'
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gui.pushContext(gui.Contexts.Credentials.Context); err != nil {
|
if err := gui.pushContext(gui.State.Contexts.Credentials); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +39,19 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
|||||||
return userInput + "\n"
|
return userInput + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleSubmitCredential() error {
|
||||||
message := gui.trimmedContent(v)
|
credentialsView := gui.Views.Credentials
|
||||||
|
message := strings.TrimSpace(credentialsView.TextArea.GetContent())
|
||||||
gui.credentials <- message
|
gui.credentials <- message
|
||||||
gui.clearEditorView(v)
|
credentialsView.ClearTextArea()
|
||||||
if err := gui.returnFromContext(); err != nil {
|
if err := gui.returnFromContext(); err != nil {
|
||||||
return err
|
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 <- ""
|
gui.credentials <- ""
|
||||||
return gui.returnFromContext()
|
return gui.returnFromContext()
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ func (gui *Gui) handleCredentialsViewFocused() error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
gui.renderString("options", message)
|
gui.renderString(gui.Views.Options, message)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
|||||||
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
|
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
|
||||||
errMessage = gui.Tr.PassUnameWrong
|
errMessage = gui.Tr.PassUnameWrong
|
||||||
}
|
}
|
||||||
|
_ = gui.returnFromContext()
|
||||||
// we are not logging this error because it may contain a password or a passphrase
|
// we are not logging this error because it may contain a password or a passphrase
|
||||||
_ = gui.createErrorPanel(errMessage)
|
_ = gui.createErrorPanel(errMessage)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+230
-94
@@ -1,50 +1,238 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CustomCommandObjects struct {
|
type CustomCommandObjects struct {
|
||||||
SelectedLocalCommit *models.Commit
|
SelectedLocalCommit *models.Commit
|
||||||
SelectedReflogCommit *models.Commit
|
SelectedReflogCommit *models.Commit
|
||||||
SelectedSubCommit *models.Commit
|
SelectedSubCommit *models.Commit
|
||||||
SelectedFile *models.File
|
SelectedFile *models.File
|
||||||
SelectedLocalBranch *models.Branch
|
SelectedPath string
|
||||||
SelectedRemoteBranch *models.RemoteBranch
|
SelectedLocalBranch *models.Branch
|
||||||
SelectedRemote *models.Remote
|
SelectedRemoteBranch *models.RemoteBranch
|
||||||
SelectedTag *models.Tag
|
SelectedRemote *models.Remote
|
||||||
SelectedStashEntry *models.StashEntry
|
SelectedTag *models.Tag
|
||||||
SelectedCommitFile *models.CommitFile
|
SelectedStashEntry *models.StashEntry
|
||||||
CheckedOutBranch *models.Branch
|
SelectedCommitFile *models.CommitFile
|
||||||
PromptResponses []string
|
SelectedCommitFilePath string
|
||||||
|
CheckedOutBranch *models.Branch
|
||||||
|
PromptResponses []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandMenuEntry struct {
|
||||||
|
label string
|
||||||
|
value string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
||||||
objects := CustomCommandObjects{
|
objects := CustomCommandObjects{
|
||||||
SelectedFile: gui.getSelectedFile(),
|
SelectedFile: gui.getSelectedFile(),
|
||||||
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
SelectedPath: gui.getSelectedPath(),
|
||||||
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
||||||
SelectedLocalBranch: gui.getSelectedBranch(),
|
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
||||||
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
SelectedLocalBranch: gui.getSelectedBranch(),
|
||||||
SelectedRemote: gui.getSelectedRemote(),
|
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
||||||
SelectedTag: gui.getSelectedTag(),
|
SelectedRemote: gui.getSelectedRemote(),
|
||||||
SelectedStashEntry: gui.getSelectedStashEntry(),
|
SelectedTag: gui.getSelectedTag(),
|
||||||
SelectedCommitFile: gui.getSelectedCommitFile(),
|
SelectedStashEntry: gui.getSelectedStashEntry(),
|
||||||
SelectedSubCommit: gui.getSelectedSubCommit(),
|
SelectedCommitFile: gui.getSelectedCommitFile(),
|
||||||
CheckedOutBranch: gui.currentBranch(),
|
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
|
||||||
PromptResponses: promptResponses,
|
SelectedSubCommit: gui.getSelectedSubCommit(),
|
||||||
|
CheckedOutBranch: gui.currentBranch(),
|
||||||
|
PromptResponses: promptResponses,
|
||||||
}
|
}
|
||||||
|
|
||||||
return utils.ResolveTemplate(templateStr, objects)
|
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 {
|
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
|
||||||
return func() error {
|
return func() error {
|
||||||
promptResponses := make([]string, len(customCommand.Prompts))
|
promptResponses := make([]string, len(customCommand.Prompts))
|
||||||
@@ -56,8 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
if customCommand.Subprocess {
|
if customCommand.Subprocess {
|
||||||
gui.PrepareSubProcess(cmdStr)
|
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingText := customCommand.LoadingText
|
loadingText := customCommand.LoadingText
|
||||||
@@ -65,9 +252,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
|||||||
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
||||||
}
|
}
|
||||||
return gui.WithWaitingStatus(loadingText, func() error {
|
return gui.WithWaitingStatus(loadingText, func() error {
|
||||||
gui.OSCommand.PrepareSubProcess(cmdStr)
|
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
|
||||||
|
|
||||||
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
|
||||||
return gui.surfaceError(err)
|
return gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
return gui.refreshSidePanels(refreshOptions{})
|
return gui.refreshSidePanels(refreshOptions{})
|
||||||
@@ -88,72 +273,18 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
|||||||
switch prompt.Type {
|
switch prompt.Type {
|
||||||
case "input":
|
case "input":
|
||||||
f = func() error {
|
f = func() error {
|
||||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
|
||||||
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()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
case "menu":
|
case "menu":
|
||||||
f = func() error {
|
f = func() error {
|
||||||
// need to make a menu here some how
|
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
|
||||||
menuItems := make([]*menuItem, len(prompt.Options))
|
}
|
||||||
for i, option := range prompt.Options {
|
case "menuFromCommand":
|
||||||
option := option
|
f = func() error {
|
||||||
|
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
|
||||||
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})
|
|
||||||
}
|
}
|
||||||
default:
|
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 "":
|
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)
|
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:
|
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 {
|
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.
|
// 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
|
// 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,
|
Contexts: contexts,
|
||||||
Key: gui.getKey(customCommand.Key),
|
Key: gui.getKey(customCommand.Key),
|
||||||
Modifier: gocui.ModNone,
|
Modifier: gocui.ModNone,
|
||||||
Handler: gui.wrappedHandler(gui.handleCustomCommandKeybinding(customCommand)),
|
Handler: gui.handleCustomCommandKeybinding(customCommand),
|
||||||
Description: description,
|
Description: description,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
||||||
|
type scenario struct {
|
||||||
|
testName string
|
||||||
|
cmdOut string
|
||||||
|
filter string
|
||||||
|
valueFormat string
|
||||||
|
labelFormat string
|
||||||
|
test func([]commandMenuEntry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []scenario{
|
||||||
|
{
|
||||||
|
"Extract remote branch name",
|
||||||
|
"upstream/pr-1",
|
||||||
|
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
|
||||||
|
"{{ .branch }}",
|
||||||
|
"Remote: {{ .remote }}",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "pr-1", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Multiple named groups with empty labelFormat",
|
||||||
|
"upstream/pr-1",
|
||||||
|
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||||
|
"{{ .branch }}|{{ .remote }}",
|
||||||
|
"",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Multiple named groups with group ids",
|
||||||
|
"upstream/pr-1",
|
||||||
|
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||||
|
"{{ .group_2 }}|{{ .group_1 }}",
|
||||||
|
"Remote: {{ .group_1 }}",
|
||||||
|
func(actualEntry []commandMenuEntry, err error) {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||||
|
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
t.Run(s.testName, func(t *testing.T) {
|
||||||
|
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user