Compare commits
549 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d86278507 | ||
|
|
985196f5aa | ||
|
|
9ec5a04cf5 | ||
|
|
bb9698810d | ||
|
|
7f4371ad71 | ||
|
|
74144e3892 | ||
|
|
07f87eb7cf | ||
|
|
5600da9ee7 | ||
|
|
ba0cc20e22 | ||
|
|
717913e64c | ||
|
|
5a0431bb62 | ||
|
|
1e357e2362 | ||
|
|
c3b62a555c | ||
|
|
557461c016 | ||
|
|
c2e670104e | ||
|
|
bea9971f9c | ||
|
|
a8fdf1a646 | ||
|
|
f8ab4f4073 | ||
|
|
24f15742d0 | ||
|
|
700f8c7e79 | ||
|
|
28e8d6e472 | ||
|
|
6076a75643 | ||
|
|
b46e4b4976 | ||
|
|
99eca7b000 | ||
|
|
a0faaf6893 | ||
|
|
56ad07ebab | ||
|
|
1ecd74c357 | ||
|
|
ceab9706cb | ||
|
|
1cc7e9c02a | ||
|
|
63e400647a | ||
|
|
6f7de83bce | ||
|
|
5af03b6820 | ||
|
|
a8866b158b | ||
|
|
0d7a697e86 | ||
|
|
e80371fc6f | ||
|
|
9cef98f779 | ||
|
|
ba6dedfb22 | ||
|
|
ba7e6add86 | ||
|
|
a820189450 | ||
|
|
ce95e6771a | ||
|
|
e9268d1828 | ||
|
|
db2e2160a9 | ||
|
|
08395ae76c | ||
|
|
4188786749 | ||
|
|
cf41338a9f | ||
|
|
a2d40cfbf1 | ||
|
|
34d1648bd3 | ||
|
|
906f8e252e | ||
|
|
986774e5c7 | ||
|
|
98763e98cb | ||
|
|
f777c60ea4 | ||
|
|
557009e660 | ||
|
|
af876d2be2 | ||
|
|
422b263df4 | ||
|
|
4fc290b101 | ||
|
|
c1bf1e52b0 | ||
|
|
4d745fa525 | ||
|
|
f0e19690f5 | ||
|
|
580c1cbc89 | ||
|
|
e21f739f4f | ||
|
|
97ad4a1643 | ||
|
|
cbafadd48e | ||
|
|
7b84c162f4 | ||
|
|
f29c81fb5c | ||
|
|
59003c8bbf | ||
|
|
6c1d133315 | ||
|
|
33ea093d88 | ||
|
|
a81f8b84e3 | ||
|
|
3f68fe42cb | ||
|
|
172cd7c687 | ||
|
|
df3e7abd68 | ||
|
|
8c67578063 | ||
|
|
06846ef3ae | ||
|
|
43ad9a81c2 | ||
|
|
9f7775df26 | ||
|
|
c1984528c8 | ||
|
|
624d63d2fa | ||
|
|
67d99a24ea | ||
|
|
bf8514f5e2 | ||
|
|
93e88ea8fe | ||
|
|
a2073528f4 | ||
|
|
230a5afa4c | ||
|
|
c49e4dc287 | ||
|
|
59f50010b6 | ||
|
|
b5827b7d80 | ||
|
|
36874be45b | ||
|
|
83b7c60246 | ||
|
|
323016aa01 | ||
|
|
b403b6e46d | ||
|
|
359636c1aa | ||
|
|
1fa55875e2 | ||
|
|
5177e458ef | ||
|
|
314c8c279a | ||
|
|
20073d0293 | ||
|
|
90a4cada82 | ||
|
|
e376de6d1a | ||
|
|
265d7e121a | ||
|
|
7ec5b6cc30 | ||
|
|
9ceaf5b9a9 | ||
|
|
cc3fa4b79d | ||
|
|
653d590157 | ||
|
|
8a01d11202 | ||
|
|
28a9594ef7 | ||
|
|
77623db1d0 | ||
|
|
6a99d36ae1 | ||
|
|
2416f585ce | ||
|
|
741e28d01a | ||
|
|
19a8029795 | ||
|
|
796f17eef4 | ||
|
|
8fbb3aea4f | ||
|
|
6fc4cb1b96 | ||
|
|
3c1935fee4 | ||
|
|
dfb87d34dc | ||
|
|
4ab1a1f72b | ||
|
|
a9cd277070 | ||
|
|
5c1463313d | ||
|
|
0355fc3008 | ||
|
|
39f065207e | ||
|
|
9e6a4a529a | ||
|
|
87de803a6c | ||
|
|
3cafa2bb12 | ||
|
|
d31520261f | ||
|
|
ad880e2d56 | ||
|
|
865809e625 | ||
|
|
04d5a473d7 | ||
|
|
f127ae62bb | ||
|
|
3f14b764d5 | ||
|
|
ae0d88f855 | ||
|
|
b65fa852f1 | ||
|
|
42500817e0 | ||
|
|
d8aba3aeee | ||
|
|
1b8836e92d | ||
|
|
54326907c3 | ||
|
|
cda7b374e2 | ||
|
|
e889a40caf | ||
|
|
3f26ddc06f | ||
|
|
dac7c90483 | ||
|
|
66e5dacf5e | ||
|
|
e3ed899b20 | ||
|
|
d6b4d4b063 | ||
|
|
45fa257128 | ||
|
|
99840d8fc4 | ||
|
|
85012dbc8f | ||
|
|
13f9073552 | ||
|
|
49b507d2ff | ||
|
|
8247fd69c9 | ||
|
|
983d0bd586 | ||
|
|
ca9ce22693 | ||
|
|
cff1dee6dc | ||
|
|
2181a91fea | ||
|
|
8c2b8cfb51 | ||
|
|
145cba34a0 | ||
|
|
7e1e97d050 | ||
|
|
320ccdb22a | ||
|
|
db1d5328f2 | ||
|
|
da2d3f253f | ||
|
|
b4323c029f | ||
|
|
203ad29349 | ||
|
|
04735d0601 | ||
|
|
bbe603ff5b | ||
|
|
23a9f41d9d | ||
|
|
1901901d24 | ||
|
|
f861175f83 | ||
|
|
25c60b1854 | ||
|
|
38557f131d | ||
|
|
96eef7838e | ||
|
|
2bf536265a | ||
|
|
43f612feb1 | ||
|
|
a1c6adab59 | ||
|
|
e72d090c5c | ||
|
|
a2a9e0c478 | ||
|
|
d3953a1440 | ||
|
|
a251cffb9a | ||
|
|
b7f6bcb3ca | ||
|
|
55c6af258d | ||
|
|
540edc0c35 | ||
|
|
12261ceb05 | ||
|
|
f6ab11e4ee | ||
|
|
2d9f7009fa | ||
|
|
4b19b4108e | ||
|
|
a23753dc18 | ||
|
|
caf99d2d64 | ||
|
|
2273f4c0a5 | ||
|
|
23fe0290ad | ||
|
|
ed2dcd9e46 | ||
|
|
75e08993ea | ||
|
|
0b07cd19f7 | ||
|
|
38f11f1f4a | ||
|
|
f91e2b12db | ||
|
|
364c1ac5e7 | ||
|
|
883fcf1083 | ||
|
|
a891bc90b7 | ||
|
|
7a74bc504b | ||
|
|
32f4d09e89 | ||
|
|
a5adfaee8a | ||
|
|
57decdd11d | ||
|
|
87dc2bcad9 | ||
|
|
21f6e9ba87 | ||
|
|
f24c95aede | ||
|
|
93ab892bdd | ||
|
|
ee7f88e123 | ||
|
|
60422912c8 | ||
|
|
6c389df57d | ||
|
|
22de5e7b23 | ||
|
|
bcbeec1a56 | ||
|
|
5628eae502 | ||
|
|
2cdd439286 | ||
|
|
110ff38c0d | ||
|
|
2680b2f4fe | ||
|
|
3e8ef0d12d | ||
|
|
bee4d98f52 | ||
|
|
584d6b241c | ||
|
|
37681627ab | ||
|
|
810155ef2f | ||
|
|
924a9bb2c9 | ||
|
|
4d635cd1cd | ||
|
|
3d49ab6666 | ||
|
|
eff931a138 | ||
|
|
cd4063c763 | ||
|
|
646c205227 | ||
|
|
dc911906b3 | ||
|
|
182e475116 | ||
|
|
5f6b61d28c | ||
|
|
810540edfa | ||
|
|
042c83387e | ||
|
|
c6a8899060 | ||
|
|
da4c12bf9e | ||
|
|
d6ee413587 | ||
|
|
9b63887867 | ||
|
|
d35eaa062b | ||
|
|
0c27776a83 | ||
|
|
4972a4c218 | ||
|
|
e4070ccb4f | ||
|
|
483b4d939d | ||
|
|
45fea83771 | ||
|
|
2aa89ade0d | ||
|
|
37029f7db3 | ||
|
|
954dfb12e4 | ||
|
|
8364509d1f | ||
|
|
d938a437a2 | ||
|
|
3c0fb9b324 | ||
|
|
8e3df6b981 | ||
|
|
5dd049eb82 | ||
|
|
f1a4a7e1ff | ||
|
|
108815c790 | ||
|
|
5c65066cbb | ||
|
|
64cf8f5b10 | ||
|
|
5fcbe4ff8a | ||
|
|
fa8248bd70 | ||
|
|
65c5a6014b | ||
|
|
fc126a1718 | ||
|
|
bb86a3ff7c | ||
|
|
bbf7a9d790 | ||
|
|
fbfa48f0fc | ||
|
|
e8b12a086c | ||
|
|
766197de9d | ||
|
|
1e44001910 | ||
|
|
317926c808 | ||
|
|
4d2346f80a | ||
|
|
d2bdac29aa | ||
|
|
cea736e6e9 | ||
|
|
e6712832b5 | ||
|
|
aa4d739577 | ||
|
|
51558f51ab | ||
|
|
35884f81e9 | ||
|
|
2008607108 | ||
|
|
21be9fb3b1 | ||
|
|
80fac559c2 | ||
|
|
e6a6301144 | ||
|
|
c5d4024d58 | ||
|
|
f91b4067f4 | ||
|
|
b46d174f70 | ||
|
|
cdc6d45fa4 | ||
|
|
81b07daa01 | ||
|
|
93266cc2a4 | ||
|
|
60fc24eada | ||
|
|
6978785ccf | ||
|
|
cd9eada0c6 | ||
|
|
7b3679d546 | ||
|
|
c8e63894b4 | ||
|
|
af93d04479 | ||
|
|
29f2bdbba3 | ||
|
|
c90e865e34 | ||
|
|
b104dccab2 | ||
|
|
2dbd5be24e | ||
|
|
0a4cf6a544 | ||
|
|
5f5e275a0e | ||
|
|
7b85d146af | ||
|
|
fd01cdb137 | ||
|
|
9e8dc37308 | ||
|
|
08666889f4 | ||
|
|
13ac1d151a | ||
|
|
c091401571 | ||
|
|
0174375562 | ||
|
|
1f756d3d0a | ||
|
|
6473e5ca3c | ||
|
|
896dda3adf | ||
|
|
b9ef1a4d67 | ||
|
|
671c693459 | ||
|
|
41171304b2 | ||
|
|
f025b289f0 | ||
|
|
018b43163c | ||
|
|
9a923eb300 | ||
|
|
3f5c1a4243 | ||
|
|
db7e623c0c | ||
|
|
ad8f02b986 | ||
|
|
29431ddc8e | ||
|
|
a1a828a781 | ||
|
|
6b150a4be0 | ||
|
|
284c534251 | ||
|
|
10fdb5a609 | ||
|
|
4dc6d40b5a | ||
|
|
99d40c2f8e | ||
|
|
0f145093fe | ||
|
|
bd91b9e1e9 | ||
|
|
bf5f3bb972 | ||
|
|
03a7e32694 | ||
|
|
b0e08491ac | ||
|
|
aaa8558de8 | ||
|
|
633553bcf8 | ||
|
|
933aae7da1 | ||
|
|
3b1689727a | ||
|
|
c08e6d9999 | ||
|
|
dcd3bb6bbd | ||
|
|
bd15f9d27a | ||
|
|
52033b32f7 | ||
|
|
5c738fa493 | ||
|
|
9a0f29ff5b | ||
|
|
36eed80228 | ||
|
|
2d80a83d27 | ||
|
|
f4e701611b | ||
|
|
ff15d86ced | ||
|
|
8b57b21d79 | ||
|
|
64d8a55dbd | ||
|
|
563efc41c7 | ||
|
|
090537a1f1 | ||
|
|
fcf616bd62 | ||
|
|
74d81ae080 | ||
|
|
a7755ab184 | ||
|
|
faf218f465 | ||
|
|
90746502df | ||
|
|
df7d1df4cb | ||
|
|
5819e04c53 | ||
|
|
fd3ce21576 | ||
|
|
3a31b84d1a | ||
|
|
f09515867d | ||
|
|
88e1a815fe | ||
|
|
db94dde114 | ||
|
|
ee4660af97 | ||
|
|
59ab38fff6 | ||
|
|
adc906bb87 | ||
|
|
9abbfe5a43 | ||
|
|
9112278ab7 | ||
|
|
d00c46a712 | ||
|
|
29ed971558 | ||
|
|
8d99b400fd | ||
|
|
7c33c02930 | ||
|
|
50b41bfccc | ||
|
|
295093a432 | ||
|
|
10c53162ca | ||
|
|
7e926cf41d | ||
|
|
d12cc5a74e | ||
|
|
8418fa17a5 | ||
|
|
905e6c16ba | ||
|
|
db140842f3 | ||
|
|
7002ee5d9b | ||
|
|
0687c07f50 | ||
|
|
922f4ed5a4 | ||
|
|
3dba246029 | ||
|
|
be3f5846e4 | ||
|
|
38a1a00cf1 | ||
|
|
9c97b75aad | ||
|
|
883f436b0f | ||
|
|
d923796cff | ||
|
|
ba2b6fbf1f | ||
|
|
73a1682540 | ||
|
|
8e22d569a0 | ||
|
|
4d0702fba5 | ||
|
|
5cbacb0c67 | ||
|
|
0568b32f0b | ||
|
|
6e518142b4 | ||
|
|
0c39347224 | ||
|
|
dd7e93ac8d | ||
|
|
f792f74f0f | ||
|
|
5ad97add08 | ||
|
|
652237d48f | ||
|
|
6a3f8eefa5 | ||
|
|
17dcfbcc6f | ||
|
|
dfafb98871 | ||
|
|
8cc8c4c228 | ||
|
|
574b34930c | ||
|
|
77080f44a4 | ||
|
|
97daa5eeb2 | ||
|
|
c476dfc1cb | ||
|
|
bde6182c94 | ||
|
|
efb049cd24 | ||
|
|
88af0fb1b6 | ||
|
|
5f30f07ea5 | ||
|
|
c02f474bf3 | ||
|
|
b32173b0c7 | ||
|
|
1c750fdb40 | ||
|
|
9ecd7908aa | ||
|
|
ad3d332e05 | ||
|
|
842ceec9b0 | ||
|
|
95c7df4c61 | ||
|
|
c0a1f90604 | ||
|
|
15528dc341 | ||
|
|
7ecbd7fbb3 | ||
|
|
45f640941c | ||
|
|
047892962a | ||
|
|
d4f4b46a1f | ||
|
|
f549ad0f37 | ||
|
|
f2dfcb6e12 | ||
|
|
65eb3780a0 | ||
|
|
d9959eb998 | ||
|
|
ebfed34145 | ||
|
|
450f269f02 | ||
|
|
95d2b02664 | ||
|
|
59e5545f38 | ||
|
|
62fc831407 | ||
|
|
fb0004481b | ||
|
|
3bd0246e4d | ||
|
|
12de0345e4 | ||
|
|
9e725ae24e | ||
|
|
97cff65612 | ||
|
|
a32fc34a49 | ||
|
|
f9c39ad64b | ||
|
|
1571e6b962 | ||
|
|
84b27d402d | ||
|
|
e6beb5d50b | ||
|
|
1354885db0 | ||
|
|
360bfa4642 | ||
|
|
bb0e580a27 | ||
|
|
1eb0061357 | ||
|
|
4cd2d70659 | ||
|
|
35d695fd0c | ||
|
|
e8eb78617c | ||
|
|
e65ddd7b6f | ||
|
|
c01bc09442 | ||
|
|
dcd461d29f | ||
|
|
98c22a36fd | ||
|
|
7bfd8155c2 | ||
|
|
ed1518fd66 | ||
|
|
400301e8fa | ||
|
|
f97a098c6f | ||
|
|
2b3bdc8b8f | ||
|
|
7323e08fdb | ||
|
|
838ed4f170 | ||
|
|
feba6b5318 | ||
|
|
27234c1cab | ||
|
|
8f6329c402 | ||
|
|
0ac56f5ade | ||
|
|
471a7de418 | ||
|
|
77191ea67c | ||
|
|
176aa62fe0 | ||
|
|
1419be1aab | ||
|
|
0ac306fe2a | ||
|
|
f0840c0f46 | ||
|
|
6d4d6d3ac0 | ||
|
|
2dba6f6733 | ||
|
|
aec125df70 | ||
|
|
9626ebdf35 | ||
|
|
2c140445e5 | ||
|
|
62a231abb7 | ||
|
|
753ca75e55 | ||
|
|
5c8f0d4c9d | ||
|
|
000a709783 | ||
|
|
5c185572a7 | ||
|
|
bb86d527e1 | ||
|
|
0e3dec1069 | ||
|
|
7e141283f6 | ||
|
|
e46f3b3393 | ||
|
|
73e740d1ba | ||
|
|
f1eaeec9ee | ||
|
|
a713fff362 | ||
|
|
6d3d40c41f | ||
|
|
2137d3f6d2 | ||
|
|
8ae346787a | ||
|
|
b8daf71db6 | ||
|
|
fd13cf14b4 | ||
|
|
47bf649a69 | ||
|
|
3b018e040f | ||
|
|
b2fbccd392 | ||
|
|
542fd8e88a | ||
|
|
9fbc332c9a | ||
|
|
1ef794e09f | ||
|
|
bfa47d3b91 | ||
|
|
48cea4e1c4 | ||
|
|
7aa884ed8f | ||
|
|
7eb673e574 | ||
|
|
f20121cb0b | ||
|
|
aa4160d57a | ||
|
|
8bc6832546 | ||
|
|
4853d186ee | ||
|
|
c3611df8d8 | ||
|
|
59650cff26 | ||
|
|
f9abcdb159 | ||
|
|
28505dddaf | ||
|
|
95b7c1d0a4 | ||
|
|
eb9f01ecfa | ||
|
|
a8c6f31bc6 | ||
|
|
1fded005c4 | ||
|
|
c470c1f575 | ||
|
|
d08241b2ea | ||
|
|
de7ab80539 | ||
|
|
d3abd9eab1 | ||
|
|
3f89b5bf71 | ||
|
|
87872f5514 | ||
|
|
bf9a53fada | ||
|
|
3adf5368dd | ||
|
|
ed6f21ee74 | ||
|
|
75a186614a | ||
|
|
18aa5b1bbe | ||
|
|
89209d1253 | ||
|
|
bcad80250a | ||
|
|
44eaccfd14 | ||
|
|
9537645d0c | ||
|
|
d08e3a55a1 | ||
|
|
2cd0bd8125 | ||
|
|
cc31cb1abe | ||
|
|
a56643fe64 | ||
|
|
2386283e45 | ||
|
|
58977ed7f3 | ||
|
|
a068548bcb | ||
|
|
b2f8b8b345 | ||
|
|
5f70b2d9cd | ||
|
|
f33b2b2277 | ||
|
|
dbf65a422a | ||
|
|
ace8544512 | ||
|
|
4832d365f1 | ||
|
|
bebe94b4b3 | ||
|
|
750445dc8b | ||
|
|
1f5f80b1bf | ||
|
|
5cc34e7801 | ||
|
|
ce8884f509 | ||
|
|
481a05f116 | ||
|
|
6e8abbcdda | ||
|
|
273eb6244b | ||
|
|
467741fa54 | ||
|
|
a00bbf709f | ||
|
|
b90e66dca1 | ||
|
|
77eea90b0b | ||
|
|
79b012990b | ||
|
|
45c0b9404a | ||
|
|
50924f31b0 | ||
|
|
705d273ad3 | ||
|
|
30c6fc2300 | ||
|
|
16b202f227 | ||
|
|
7c1dbb7c66 |
74
.circleci/config.yml
Normal file
74
.circleci/config.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Ensure go.mod file is up to date
|
||||
command: |
|
||||
export GO111MODULE=on
|
||||
rm go.sum
|
||||
mv go.mod /tmp/
|
||||
go mod init
|
||||
export GO111MODULE=auto
|
||||
|
||||
if [ $(diff /tmp/go.mod go.mod|wc -l) -gt 0 ]; then
|
||||
diff /tmp/go.mod go.mod
|
||||
exit 1;
|
||||
fi
|
||||
- run:
|
||||
name: Run gofmt -s
|
||||
command: |
|
||||
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
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pkg-cache-{{ checksum "Gopkg.lock" }}-v2
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
./test.sh
|
||||
- run:
|
||||
name: Compile project on every platform
|
||||
command: |
|
||||
go get github.com/mitchellh/gox
|
||||
gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
|
||||
- run:
|
||||
name: Push on codecov result
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
- save_cache:
|
||||
key: pkg-cache-{{ checksum "Gopkg.lock" }}-v2
|
||||
paths:
|
||||
- ~/.cache/go-build
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run gorelease
|
||||
command: |
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build
|
||||
release:
|
||||
jobs:
|
||||
- release:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows]
|
||||
- Lazygit Version [e.g. v0.1.45]
|
||||
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1,6 +1,26 @@
|
||||
development.log
|
||||
commands.log
|
||||
extra/lgit.rb
|
||||
notes/go.notes
|
||||
TODO.notes
|
||||
TODO.md
|
||||
# Please do not add personal files
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Hidden
|
||||
.*
|
||||
|
||||
# TODO
|
||||
TODO.*
|
||||
|
||||
# Notes
|
||||
*.notes
|
||||
|
||||
# Tests
|
||||
test/repos/repo
|
||||
coverage.txt
|
||||
|
||||
# Binaries
|
||||
lazygit
|
||||
|
||||
# Exceptions
|
||||
!.gitignore
|
||||
!.goreleaser.yml
|
||||
!.circleci/
|
||||
!.github/
|
||||
@@ -1,34 +1,43 @@
|
||||
# This is an example goreleaser.yaml file with some sane defaults.
|
||||
# Make sure to check the documentation at http://goreleaser.com
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- freebsd
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- freebsd
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- 386
|
||||
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
|
||||
ldflags:
|
||||
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
|
||||
|
||||
archive:
|
||||
replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
386: 32-bit
|
||||
amd64: x86_64
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
name_template: '{{ .Tag }}-next'
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^bump'
|
||||
brew:
|
||||
# Reporitory to push the tap to.
|
||||
github:
|
||||
@@ -37,18 +46,19 @@ brew:
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: "https://github.com/jesseduffield/lazygit/"
|
||||
homepage: 'https://github.com/jesseduffield/lazygit/'
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: "A simple terminal UI for git commands, written in Go"
|
||||
description: 'A simple terminal UI for git commands, written in Go'
|
||||
|
||||
# # Packages your package depends on.
|
||||
# dependencies:
|
||||
# - git
|
||||
# - zsh
|
||||
|
||||
# # Packages that conflict with your package.
|
||||
# conflicts:
|
||||
# - svn
|
||||
# - bash
|
||||
|
||||
# test comment to see if goreleaser only releases on new commits
|
||||
|
||||
@@ -15,9 +15,10 @@ welcome your pull requests:
|
||||
1. Fork the repo and create your branch from `master`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. If you've added code that need documentation, update the documentation.
|
||||
4. Be sure to test your modifications.
|
||||
5. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
6. Issue that pull request!
|
||||
4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible.
|
||||
5. Be sure to test your modifications.
|
||||
6. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
7. Issue that pull request!
|
||||
|
||||
## Code of conduct
|
||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest
|
||||
|
||||
FROM golang:alpine
|
||||
|
||||
RUN apk add -U git xdg-utils
|
||||
|
||||
ADD . /go/src/github.com/jesseduffield/lazygit
|
||||
|
||||
RUN go install github.com/jesseduffield/lazygit
|
||||
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit
|
||||
562
Gopkg.lock
generated
562
Gopkg.lock
generated
@@ -1,6 +1,85 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e24ea5dbc89fbab51635ee32e5be4f61a9267cae20788efcae4c07efb4abec99"
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
"aws",
|
||||
"aws/awserr",
|
||||
"aws/awsutil",
|
||||
"aws/client",
|
||||
"aws/client/metadata",
|
||||
"aws/corehandlers",
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
"private/protocol/restxml",
|
||||
"private/protocol/xml/xmlutil",
|
||||
"service/s3",
|
||||
"service/sts",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "4324bc9d8865bdb3e6aa86ec7772ca1272d2750e"
|
||||
version = "v1.15.21"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:37011b20a70e205b93ebea5287e1afa5618db54bf3998c36ff5a8e4b146a170a"
|
||||
name = "github.com/bgentry/go-netrc"
|
||||
packages = ["netrc"]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
|
||||
name = "github.com/cloudfoundry/jibber_jabber"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "NUT"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
|
||||
name = "github.com/emirpasic/gods"
|
||||
packages = [
|
||||
"containers",
|
||||
"lists",
|
||||
"lists/arraylist",
|
||||
"trees",
|
||||
"trees/binaryheap",
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
|
||||
name = "github.com/fatih/color"
|
||||
@@ -9,6 +88,22 @@
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1b91ae0dc69a41d4c2ed23ea5cffb721ea63f5037ca4b81e6d6771fbb8f45129"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:74d9b0a7b4107b41e0ade759fac64502876f82d29fb23d77b3dd24b194ee3dd5"
|
||||
name = "github.com/go-ini/ini"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e"
|
||||
version = "v1.38.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
|
||||
@@ -19,11 +114,117 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:82b3bbc50ba7b6065b0229ebf0fc990a76e47410acd510294e435aeb3523293e"
|
||||
digest = "1:a5d940c38bf56f121721bfa747c66356df387cb9d5318c570c6d4170aab62862"
|
||||
name = "github.com/hashicorp/go-cleanhttp"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b634d733abf079dc191d359e5a8d31479f1795d00e656f8a018a459571046266"
|
||||
name = "github.com/hashicorp/go-getter"
|
||||
packages = ["helper/url"]
|
||||
pruneopts = "NUT"
|
||||
revision = "4bda8fa99001c61db3cad96b421d4c12a81f256d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fbab03227343a0285fc74a68dd2ff46cda7edecbbe5a3e98d2cecd00cc67b217"
|
||||
name = "github.com/hashicorp/go-safetemp"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "b1a1dbde6fdc11e3ae79efd9039009e22d4ae240"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0b06ffe0c0764e413a6738e3f045d6bb14117359aef80a09f8c60fbff2ecad6b"
|
||||
name = "github.com/hashicorp/go-version"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:11c6c696067d3127ecf332b10f89394d386d9083f82baf71f40f2da31841a009"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d457d39e88f678ed14ac29517c3d74927a48dbc6a9f073fa241cf364a68cbe5c"
|
||||
name = "github.com/heroku/rollrus"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "fc0cef2ff331aebb24cd4e9ded7e20650f3d7006"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d"
|
||||
name = "github.com/jbenet/go-context"
|
||||
packages = ["io"]
|
||||
pruneopts = "NUT"
|
||||
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:490643e333b848f3d6ab772c21082d706663dcf4a3c0fbe9a4b4ef7b205ce6c7"
|
||||
name = "github.com/jesseduffield/go-getter"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "906e15686e6309ff310c1c10463ab53287c3a678"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:71e6c15797951d3fabaa944d70253e36a6cee96bf54ca0bc43ca3de3b4814bbb"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3c923f53ac9952af649af04a067405843558d56f"
|
||||
revision = "2cb6e95bbbf850bb32cc1799e07d08ff0f144746"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "0b12d6b5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:263f9b0a0bcbfff9d5e7d9f2aa11f53995d98214fe0fb97e429e7a5f4534a0f9"
|
||||
name = "github.com/kardianos/osext"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
|
||||
name = "github.com/kevinburke/ssh_config"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
|
||||
version = "0.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c2353362d570a7bfa228149c62842019201cfb71"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
|
||||
@@ -49,6 +250,50 @@
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
|
||||
name = "github.com/mgutz/str"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
|
||||
name = "github.com/mitchellh/go-testing-interface"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"v2/i18n",
|
||||
"v2/internal",
|
||||
"v2/internal/plural",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
|
||||
version = "v2.0.0-beta.5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:34d9354c2c5d916c05864327553047df59fc10e86ff1f408e4136eba0a25a5ec"
|
||||
@@ -57,6 +302,141 @@
|
||||
pruneopts = "NUT"
|
||||
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
|
||||
name = "github.com/pelletier/go-buffruneio"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "NUT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
pruneopts = "NUT"
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
|
||||
name = "github.com/shibukawa/configdir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||
version = "v1.0.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0e9a5ac14bcc11f205031a671b28c7e05cb88b2ebbe06f383c1ab0b2c12c7cb5"
|
||||
name = "github.com/spkg/bom"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "59b7046e48ad6bac800c5e1dd5142282cbfcf154"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
|
||||
name = "github.com/src-d/gcfg"
|
||||
packages = [
|
||||
".",
|
||||
"scanner",
|
||||
"token",
|
||||
"types",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bacb8b590716ab7c33f2277240972c9582d389593ee8d66fc10074e0508b8126"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
pruneopts = "NUT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e42372d3f4921ec35df07f9b23239631e9d28580f7c1edcca212bc6daddc68fe"
|
||||
name = "github.com/stvp/roll"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3627a5cbeaeaa68023abd02bb8687925265f2f63"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
|
||||
name = "github.com/tcnksm/go-gitconfig"
|
||||
@@ -66,21 +446,189 @@
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4d8a79fbc6fa348fc94afa4235947c5196b7900ed71b94aa5fcbc7e273d150e1"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
digest = "1:07e8742c479bab0066149ad02a710024154e76874fd0a2dba002d87702725825"
|
||||
name = "github.com/ulikunitz/xz"
|
||||
packages = [
|
||||
".",
|
||||
"internal/hash",
|
||||
"internal/xlog",
|
||||
"lzma",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "0ffbfd41fbef8ffcf9b62b0b0aa3a5873ed7a4fe"
|
||||
revision = "0c6b41e72360850ca4f98dc341fd999726ea007f"
|
||||
version = "v0.5.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3148cb3478c26a92b4c1a18abb9428234b281e278af6267840721a24b6cbc6a3"
|
||||
name = "github.com/xanzy/ssh-agent"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cast5",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"openpgp",
|
||||
"openpgp/armor",
|
||||
"openpgp/elgamal",
|
||||
"openpgp/errors",
|
||||
"openpgp/packet",
|
||||
"openpgp/s2k",
|
||||
"poly1305",
|
||||
"ssh",
|
||||
"ssh/agent",
|
||||
"ssh/knownhosts",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context"]
|
||||
pruneopts = "NUT"
|
||||
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:47a697b155f5214ff14e68e39ce9c2e8d93e1fb035ae5ba7e247d044e0ce64e3"
|
||||
name = "gopkg.in/src-d/go-billy.v4"
|
||||
packages = [
|
||||
".",
|
||||
"helper/chroot",
|
||||
"helper/polyfill",
|
||||
"osfs",
|
||||
"util",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "83cf655d40b15b427014d7875d10850f96edba14"
|
||||
version = "v4.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e66078da2bd6e53c72518d7f6ae0c3c8c7f34c0df12c39435ce34a6bce165525"
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"internal/revision",
|
||||
"plumbing",
|
||||
"plumbing/cache",
|
||||
"plumbing/filemode",
|
||||
"plumbing/format/config",
|
||||
"plumbing/format/diff",
|
||||
"plumbing/format/gitignore",
|
||||
"plumbing/format/idxfile",
|
||||
"plumbing/format/index",
|
||||
"plumbing/format/objfile",
|
||||
"plumbing/format/packfile",
|
||||
"plumbing/format/pktline",
|
||||
"plumbing/object",
|
||||
"plumbing/protocol/packp",
|
||||
"plumbing/protocol/packp/capability",
|
||||
"plumbing/protocol/packp/sideband",
|
||||
"plumbing/revlist",
|
||||
"plumbing/storer",
|
||||
"plumbing/transport",
|
||||
"plumbing/transport/client",
|
||||
"plumbing/transport/file",
|
||||
"plumbing/transport/git",
|
||||
"plumbing/transport/http",
|
||||
"plumbing/transport/internal/common",
|
||||
"plumbing/transport/server",
|
||||
"plumbing/transport/ssh",
|
||||
"storage",
|
||||
"storage/filesystem",
|
||||
"storage/filesystem/dotgit",
|
||||
"storage/memory",
|
||||
"utils/binary",
|
||||
"utils/diff",
|
||||
"utils/ioutil",
|
||||
"utils/merkletrie",
|
||||
"utils/merkletrie/filesystem",
|
||||
"utils/merkletrie/index",
|
||||
"utils/merkletrie/internal/frame",
|
||||
"utils/merkletrie/noder",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf"
|
||||
name = "gopkg.in/warnings.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/cloudfoundry/jibber_jabber",
|
||||
"github.com/fatih/color",
|
||||
"github.com/golang-collections/collections/stack",
|
||||
"github.com/heroku/rollrus",
|
||||
"github.com/jesseduffield/go-getter",
|
||||
"github.com/jesseduffield/gocui",
|
||||
"github.com/kardianos/osext",
|
||||
"github.com/mgutz/str",
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n",
|
||||
"github.com/shibukawa/configdir",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/spf13/viper",
|
||||
"github.com/spkg/bom",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/tcnksm/go-gitconfig",
|
||||
"golang.org/x/text/language",
|
||||
"gopkg.in/src-d/go-git.v4",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing",
|
||||
"gopkg.in/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
@@ -36,3 +36,11 @@
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spkg/bom"
|
||||
|
||||
127
README.md
127
README.md
@@ -1,30 +1,46 @@
|
||||
# lazygit [](https://goreportcard.com/report/github.com/jesseduffield/lazygit)
|
||||
# lazygit [](https://circleci.com/gh/jesseduffield/lazygit) [](https://codecov.io/gh/jesseduffield/lazygit) [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/github.com/jesseduffield/lazygit) []()
|
||||
|
||||
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
|
||||
|
||||
Are YOU tired of typing every git command directly into the terminal, but you're too stubborn to use Sourcetree because you'll never forgive Atlassian for making Jira? This is the app for you!
|
||||
Are YOU tired of typing every git command directly into the terminal, but you're
|
||||
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
|
||||
Jira? This is the app for you!
|
||||
|
||||
[Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
|
||||
|
||||

|
||||

|
||||
|
||||
* [Installation](https://github.com/jesseduffield/lazygit#installation)
|
||||
* [Usage](https://github.com/jesseduffield/lazygit#usage),
|
||||
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
|
||||
* [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
|
||||
* [Contributing](https://github.com/jesseduffield/lazygit#contributing)
|
||||
* [Video Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
|
||||
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
|
||||
## Installation
|
||||
|
||||
### Via binary release
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases)
|
||||
|
||||
### Via Go
|
||||
In a terminal call this command:
|
||||
`go get github.com/jesseduffield/lazygit`
|
||||
(if you don't have Go installed, you can follow the installation guide [here](https://golang.org/doc/install).
|
||||
|
||||
Please note:
|
||||
If you get an error claiming that lazygit cannot be found or is not defined, you may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin` (Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries, not apps like Lazygit)
|
||||
### Homebrew
|
||||
```sh
|
||||
brew tap jesseduffield/lazygit
|
||||
brew install lazygit
|
||||
```
|
||||
|
||||
### Ubuntu
|
||||
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via Launchpad PPA.
|
||||
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||
|
||||
They are built daily, straight from master branch.
|
||||
**Release builds**
|
||||
|
||||
Built from git tags. Supposed to be more stable.
|
||||
|
||||
```sh
|
||||
sudo add-apt-repository ppa:lazygit-team/release
|
||||
sudo apt-get update
|
||||
sudo apt-get install lazygit
|
||||
```
|
||||
|
||||
**Daily builds**
|
||||
|
||||
Built from master branch once in 24 hours (or more sometimes).
|
||||
|
||||
```sh
|
||||
sudo add-apt-repository ppa:lazygit-team/daily
|
||||
@@ -32,30 +48,66 @@ sudo apt-get update
|
||||
sudo apt-get install lazygit
|
||||
```
|
||||
|
||||
### Void Linux
|
||||
Packages for Void Linux are available in the distro repo
|
||||
|
||||
They follow upstream latest releases
|
||||
|
||||
```sh
|
||||
sudo xbps-install -S lazygit
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
* Stable: https://aur.archlinux.org/packages/lazygit/
|
||||
* Development: https://aur.archlinux.org/packages/lazygit-git/
|
||||
|
||||
Instruction of how to install AUR content can be found here:
|
||||
https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
|
||||
### Binary Release (Windows/Linux/OSX)
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
|
||||
|
||||
### Go
|
||||
```sh
|
||||
go get github.com/jesseduffield/lazygit
|
||||
```
|
||||
|
||||
Please note:
|
||||
If you get an error claiming that lazygit cannot be found or is not defined, you
|
||||
may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin`
|
||||
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
|
||||
not apps like Lazygit).
|
||||
|
||||
## Usage
|
||||
Call `lazygit` in your terminal inside a git repository.
|
||||
If you want, you can also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or whichever rc file you're using).
|
||||
Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
|
||||
Call `lazygit` in your terminal inside a git repository. If you want, you can
|
||||
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
|
||||
whichever rc file you're using).
|
||||
|
||||
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
|
||||
* Basic video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
|
||||
* List of keybindings
|
||||
[here](/docs/Keybindings.md).
|
||||
|
||||
## Cool features
|
||||
- Adding files easily
|
||||
- Resolving merge conflicts
|
||||
- Easily check out recent branches
|
||||
- Scroll through logs/diffs of branches/commits/stash
|
||||
- Quick pushing/pulling
|
||||
- Squash down and rename commits
|
||||
* Adding files easily
|
||||
* Resolving merge conflicts
|
||||
* Easily check out recent branches
|
||||
* Scroll through logs/diffs of branches/commits/stash
|
||||
* Quick pushing/pulling
|
||||
* Squash down and rename commits
|
||||
|
||||
### Resolving merge conflicts
|
||||

|
||||

|
||||
|
||||
### Viewing commit diffs
|
||||

|
||||

|
||||
|
||||
## Milestones
|
||||
- [ ] Easy Installation (homebrew, release binaries)
|
||||
- [x] Easy Installation (homebrew, release binaries)
|
||||
- [ ] Configurable Keybindings
|
||||
- [ ] Configurable Color Themes
|
||||
- [ ] Spawning Subprocesses (help needed - have a look at https://github.com/jesseduffield/lazygit/pull/18)
|
||||
@@ -65,6 +117,21 @@ Basic tutorial [Here](https://www.youtube.com/watch?v=VDXvbHZYeKY)
|
||||
|
||||
## Contributing
|
||||
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
|
||||
For contributor discussion about things not better discussed here in the repo, join the slack channel
|
||||
|
||||
[](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
|
||||
|
||||
## Work in progress
|
||||
This is still a work in progress so there's still bugs to iron out and as this is my first project in Go the code could no doubt use an increase in quality, but I'll be improving on it whenever I find the time. If you have any feedback feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
|
||||
This is still a work in progress so there's still bugs to iron out and as this
|
||||
is my first project in Go the code could no doubt use an increase in quality,
|
||||
but I'll be improving on it whenever I find the time. If you have any feedback
|
||||
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
|
||||
|
||||
## Social
|
||||
If you want to see what I (Jesse) am up to in terms of development, follow me on
|
||||
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
|
||||
[twitch](https://www.twitch.tv/jesseduffield).
|
||||
|
||||
## Alternatives
|
||||
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
|
||||
- [tig](https://github.com/jonas/tig)
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
index := getItemPosition(v)
|
||||
if index == 0 {
|
||||
return createErrorPanel(g, "You have already checked out this branch")
|
||||
}
|
||||
branch := getSelectedBranch(v)
|
||||
if output, err := gitCheckout(branch.Name, false); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
}
|
||||
return refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := getSelectedBranch(v)
|
||||
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitCheckout(branch.Name, true); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
}
|
||||
return refreshSidePanels(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitCheckout(trimmedContent(v), false); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
return refreshSidePanels(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := state.Branches[0]
|
||||
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
refreshSidePanels(g)
|
||||
return handleBranchSelect(g, v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
checkedOutBranch := state.Branches[0]
|
||||
selectedBranch := getSelectedBranch(v)
|
||||
defer refreshSidePanels(g)
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return createErrorPanel(g, "You cannot merge a branch into itself")
|
||||
}
|
||||
if output, err := gitMerge(selectedBranch.Name); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSelectedBranch(v *gocui.View) Branch {
|
||||
lineNumber := getItemPosition(v)
|
||||
return state.Branches[lineNumber]
|
||||
}
|
||||
|
||||
func renderBranchesOptions(g *gocui.Gui) error {
|
||||
return renderOptionsMap(g, map[string]string{
|
||||
"space": "checkout",
|
||||
"f": "force checkout",
|
||||
"m": "merge",
|
||||
"c": "checkout by name",
|
||||
"n": "new branch",
|
||||
"← → ↑ ↓": "navigate",
|
||||
})
|
||||
}
|
||||
|
||||
// may want to standardise how these select methods work
|
||||
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := renderBranchesOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
// This really shouldn't happen: there should always be a master branch
|
||||
if len(state.Branches) == 0 {
|
||||
return renderString(g, "main", "No branches for this repo")
|
||||
}
|
||||
go func() {
|
||||
branch := getSelectedBranch(v)
|
||||
diff, err := getBranchGraph(branch.Name, branch.BaseBranch)
|
||||
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
||||
diff = "There is no tracking for this branch"
|
||||
}
|
||||
renderString(g, "main", diff)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshStatus is called at the end of this because that's when we can
|
||||
// be sure there is a state.Branches array to pick the current branch from
|
||||
func refreshBranches(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
v, err := g.View("branches")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
state.Branches = getGitBranches()
|
||||
v.Clear()
|
||||
for _, branch := range state.Branches {
|
||||
fmt.Fprintln(v, branch.DisplayString)
|
||||
}
|
||||
resetOrigin(v)
|
||||
return refreshStatus(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
138
commits_panel.go
138
commits_panel.go
@@ -1,138 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoCommits : When no commits are found for the branch
|
||||
ErrNoCommits = errors.New("No commits for this branch")
|
||||
)
|
||||
|
||||
func refreshCommits(g *gocui.Gui) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
state.Commits = getCommits()
|
||||
v, err := g.View("commits")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.Clear()
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
white := color.New(color.FgWhite)
|
||||
shaColor := white
|
||||
for _, commit := range state.Commits {
|
||||
if commit.Pushed {
|
||||
shaColor = red
|
||||
} else {
|
||||
shaColor = yellow
|
||||
}
|
||||
shaColor.Fprint(v, commit.Sha+" ")
|
||||
white.Fprintln(v, commit.Name)
|
||||
}
|
||||
refreshStatus(g)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
|
||||
commit, err := getSelectedCommit(g)
|
||||
devLog(commit)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if output, err := gitResetToCommit(commit.Sha); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
if err := refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := refreshFiles(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resetOrigin(commitView)
|
||||
return handleCommitSelect(g, nil)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func renderCommitsOptions(g *gocui.Gui) error {
|
||||
return renderOptionsMap(g, map[string]string{
|
||||
"s": "squash down",
|
||||
"r": "rename",
|
||||
"g": "reset to this commit",
|
||||
"← → ↑ ↓": "navigate",
|
||||
})
|
||||
}
|
||||
|
||||
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := renderCommitsOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
commit, err := getSelectedCommit(g)
|
||||
if err != nil {
|
||||
if err != ErrNoCommits {
|
||||
return err
|
||||
}
|
||||
return renderString(g, "main", "No commits for this branch")
|
||||
}
|
||||
commitText := gitShow(commit.Sha)
|
||||
return renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if getItemPosition(v) != 0 {
|
||||
return createErrorPanel(g, "Can only squash topmost commit")
|
||||
}
|
||||
if len(state.Commits) == 1 {
|
||||
return createErrorPanel(g, "You have no commits to squash with")
|
||||
}
|
||||
commit, err := getSelectedCommit(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
if err := refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
refreshStatus(g)
|
||||
return handleCommitSelect(g, v)
|
||||
}
|
||||
|
||||
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
if getItemPosition(v) != 0 {
|
||||
return createErrorPanel(g, "Can only rename topmost commit")
|
||||
}
|
||||
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitRenameCommit(v.Buffer()); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
if err := refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return handleCommitSelect(g, v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
|
||||
v, err := g.View("commits")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(state.Commits) == 0 {
|
||||
return Commit{}, ErrNoCommits
|
||||
}
|
||||
lineNumber := getItemPosition(v)
|
||||
if lineNumber > len(state.Commits)-1 {
|
||||
colorLog(color.FgRed, "potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
|
||||
return state.Commits[len(state.Commits)-1], nil
|
||||
}
|
||||
return state.Commits[lineNumber], nil
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
if function != nil {
|
||||
if err := function(g, v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return closeConfirmationPrompt(g)
|
||||
}
|
||||
}
|
||||
|
||||
func closeConfirmationPrompt(g *gocui.Gui) error {
|
||||
view, err := g.View("confirmation")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := returnFocus(g, view); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.DeleteKeybindings("confirmation")
|
||||
return g.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
func getMessageHeight(message string, width int) int {
|
||||
lines := strings.Split(message, "\n")
|
||||
lineCount := 0
|
||||
for _, line := range lines {
|
||||
lineCount += len(line)/width + 1
|
||||
}
|
||||
return lineCount
|
||||
}
|
||||
|
||||
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
||||
width, height := g.Size()
|
||||
panelWidth := 60
|
||||
panelHeight := getMessageHeight(prompt, panelWidth)
|
||||
return width/2 - panelWidth/2,
|
||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||
width/2 + panelWidth/2,
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleYes func(*gocui.Gui, *gocui.View) error) error {
|
||||
// only need to fit one line
|
||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
|
||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Cursor = true
|
||||
|
||||
confirmationView.Editable = true
|
||||
confirmationView.Title = title
|
||||
confirmationView.FgColor = gocui.ColorWhite
|
||||
switchFocus(g, currentView, confirmationView)
|
||||
return setKeyBindings(g, handleYes, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
// delete the existing confirmation panel if it exists
|
||||
if view, _ := g.View("confirmation"); view != nil {
|
||||
if err := closeConfirmationPrompt(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
|
||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
confirmationView.Title = title
|
||||
confirmationView.FgColor = gocui.ColorWhite
|
||||
renderString(g, "confirmation", prompt)
|
||||
switchFocus(g, currentView, confirmationView)
|
||||
return setKeyBindings(g, handleYes, handleNo)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func setKeyBindings(g *gocui.Gui, handleYes, handleNo func(*gocui.Gui, *gocui.View) error) error {
|
||||
renderString(g, "options", "esc: close, enter: confirm")
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleYes)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleNo))
|
||||
}
|
||||
|
||||
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
||||
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
||||
}
|
||||
|
||||
func createErrorPanel(g *gocui.Gui, message string) error {
|
||||
currentView := g.CurrentView()
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
|
||||
}
|
||||
78
docs/Config.md
Normal file
78
docs/Config.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# User Config:
|
||||
|
||||
## Default:
|
||||
|
||||
```
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
```
|
||||
|
||||
## Platform Defaults:
|
||||
|
||||
### Windows:
|
||||
|
||||
```
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux:
|
||||
|
||||
```
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX:
|
||||
|
||||
```
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Recommended Config Values:
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```
|
||||
os:
|
||||
openCommand: 'code -r {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes:
|
||||
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
The available attributes are:
|
||||
|
||||
- default
|
||||
- black
|
||||
- red
|
||||
- green
|
||||
- yellow
|
||||
- blue
|
||||
- magenta
|
||||
- cyan
|
||||
- white
|
||||
- bold
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
## Example Coloring:
|
||||
|
||||

|
||||
@@ -2,55 +2,84 @@
|
||||
|
||||
## Global:
|
||||
|
||||
← → ↑ ↓: navigate
|
||||
PgUp/PgDn: scroll diff panel (use fn+up/down on osx)
|
||||
q: quit
|
||||
p: pull
|
||||
shift+P: push
|
||||
<pre>
|
||||
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
|
||||
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
|
||||
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
|
||||
<kbd>q</kbd>: quit
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>shift</kbd>+<kbd>P</kbd>: push
|
||||
</pre>
|
||||
|
||||
## Status Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
</pre>
|
||||
|
||||
## Files Panel:
|
||||
|
||||
space: toggle staged
|
||||
c: commit changes
|
||||
shift+S: stash files
|
||||
t: add patched (i.e. pick chunks of a file to add)
|
||||
o: open
|
||||
e: edit
|
||||
s: open in sublime (requires 'subl' command)
|
||||
v: open in vscode (requires 'code' command)
|
||||
i: add to .gitignore
|
||||
d: delete if untracked checkout if tracked (aka go away)
|
||||
shift+R: refresh files
|
||||
<pre>
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
|
||||
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
|
||||
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
|
||||
<kbd>o</kbd>: open
|
||||
<kbd>e</kbd>: edit
|
||||
<kbd>s</kbd>: open in sublime (requires 'subl' command)
|
||||
<kbd>v</kbd>: open in vscode (requires 'code' command)
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
|
||||
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
|
||||
<kbd>shift</kbd>+<kbd>A</kbd>: abort merge
|
||||
</pre>
|
||||
|
||||
## Branches Panel:
|
||||
|
||||
space: checkout branch
|
||||
f: force checkout branch
|
||||
m: merge into currently checked out branch
|
||||
c: checkout by name
|
||||
n: new branch
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout branch
|
||||
<kbd>f</kbd>: force checkout branch
|
||||
<kbd>m</kbd>: merge into currently checked out branch
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>D</kbd>: force delete branch
|
||||
</pre>
|
||||
|
||||
## Commits Panel:
|
||||
|
||||
s: squash down (only available for topmost commit)
|
||||
r: rename commit
|
||||
g: reset to this commit
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash down (only available for topmost commit)
|
||||
<kbd>r</kbd>: rename commit
|
||||
<kbd>shift</kbd>+<kbd>R</kbd>: rename commit using git editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
</pre>
|
||||
|
||||
## Stash Panel:
|
||||
|
||||
space: apply
|
||||
k: pop
|
||||
d: drop
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
</pre>
|
||||
|
||||
## Popup Panel:
|
||||
|
||||
esc: close/cancel
|
||||
enter: confirm
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close/cancel
|
||||
<kbd>enter</kbd>: confirm
|
||||
<kbd>tab</kbd>: enter newline (if editing)
|
||||
</pre>
|
||||
|
||||
## Resolving Merge Conflicts (Diff Panel):
|
||||
|
||||
← →: navigate conflicts
|
||||
↑ ↓: select hunk
|
||||
space: pick hunk
|
||||
b: pick both hunks
|
||||
z: undo (only available while still inside diff panel)
|
||||
<pre>
|
||||
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
|
||||
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>z</kbd>: undo (only available while still inside diff panel)
|
||||
</pre>
|
||||
|
||||
BIN
docs/resources/colored-border-example.png
Normal file
BIN
docs/resources/colored-border-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/resources/lazygit-example.gif
Normal file
BIN
docs/resources/lazygit-example.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/resources/resolving-merge-conflicts.gif
Normal file
BIN
docs/resources/resolving-merge-conflicts.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/resources/slack_rgb.png
Normal file
BIN
docs/resources/slack_rgb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
docs/resources/viewing-commit-diffs.png
Normal file
BIN
docs/resources/viewing-commit-diffs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
353
files_panel.go
353
files_panel.go
@@ -1,353 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoFiles : when there are no modified files in the repo
|
||||
ErrNoFiles = errors.New("No changed files")
|
||||
)
|
||||
|
||||
func stagedFiles(files []GitFile) []GitFile {
|
||||
result := make([]GitFile, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func stageSelectedFile(g *gocui.Gui) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return stageFile(file.Name)
|
||||
}
|
||||
|
||||
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if file.HasMergeConflicts {
|
||||
return handleSwitchToMerge(g, v)
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
stageFile(file.Name)
|
||||
} else {
|
||||
unStageFile(file.Name, file.Tracked)
|
||||
}
|
||||
|
||||
if err := refreshFiles(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleFileSelect(g, v)
|
||||
}
|
||||
|
||||
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !file.HasUnstagedChanges {
|
||||
return createErrorPanel(g, "File has no unstaged changes to add")
|
||||
}
|
||||
if !file.Tracked {
|
||||
return createErrorPanel(g, "Cannot git add --patch untracked files")
|
||||
}
|
||||
gitAddPatch(g, file.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
|
||||
if len(state.GitFiles) == 0 {
|
||||
return GitFile{}, ErrNoFiles
|
||||
}
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
lineNumber := getItemPosition(filesView)
|
||||
return state.GitFiles[lineNumber], nil
|
||||
}
|
||||
|
||||
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var deleteVerb string
|
||||
if file.Tracked {
|
||||
deleteVerb = "checkout"
|
||||
} else {
|
||||
deleteVerb = "delete"
|
||||
}
|
||||
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := removeFile(file); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return refreshFiles(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
return createErrorPanel(g, err.Error())
|
||||
}
|
||||
if file.Tracked {
|
||||
return createErrorPanel(g, "Cannot ignore tracked files")
|
||||
}
|
||||
gitIgnore(file.Name)
|
||||
return refreshFiles(g)
|
||||
}
|
||||
|
||||
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
|
||||
optionsMap := map[string]string{
|
||||
"← → ↑ ↓": "navigate",
|
||||
"S": "stash files",
|
||||
"c": "commit changes",
|
||||
"o": "open",
|
||||
"i": "ignore",
|
||||
"d": "delete",
|
||||
"space": "toggle staged",
|
||||
"R": "refresh",
|
||||
"t": "add patch",
|
||||
"e": "edit",
|
||||
}
|
||||
if state.HasMergeConflicts {
|
||||
optionsMap["a"] = "abort merge"
|
||||
optionsMap["m"] = "resolve merge conflicts"
|
||||
}
|
||||
if gitFile == nil {
|
||||
return renderOptionsMap(g, optionsMap)
|
||||
}
|
||||
if gitFile.Tracked {
|
||||
optionsMap["d"] = "checkout"
|
||||
}
|
||||
return renderOptionsMap(g, optionsMap)
|
||||
}
|
||||
|
||||
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
gitFile, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
renderString(g, "main", "No changed files")
|
||||
return renderfilesOptions(g, nil)
|
||||
}
|
||||
renderfilesOptions(g, &gitFile)
|
||||
var content string
|
||||
if gitFile.HasMergeConflicts {
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
content = getDiff(gitFile)
|
||||
return renderString(g, "main", content)
|
||||
}
|
||||
|
||||
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
|
||||
return createErrorPanel(g, "There are no staged files to commit")
|
||||
}
|
||||
createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error {
|
||||
message := trimmedContent(v)
|
||||
if message == "" {
|
||||
return createErrorPanel(g, "You cannot commit without a commit message")
|
||||
}
|
||||
if output, err := gitCommit(g, message); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
refreshFiles(g)
|
||||
return refreshCommits(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if _, err := open(g, file.Name); err != nil {
|
||||
return createErrorPanel(g, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
return genericFileOpen(g, v, editFile)
|
||||
}
|
||||
|
||||
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
return genericFileOpen(g, v, openFile)
|
||||
}
|
||||
|
||||
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
return genericFileOpen(g, v, sublimeOpenFile)
|
||||
}
|
||||
|
||||
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
return genericFileOpen(g, v, vsCodeOpenFile)
|
||||
}
|
||||
|
||||
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return refreshFiles(g)
|
||||
}
|
||||
|
||||
func refreshStateGitFiles() {
|
||||
// get files to stage
|
||||
gitFiles := getGitStatusFiles()
|
||||
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
|
||||
updateHasMergeConflictStatus()
|
||||
}
|
||||
|
||||
func updateHasMergeConflictStatus() error {
|
||||
merging, err := isInMergeState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.HasMergeConflicts = merging
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
if !gitFile.Tracked && !gitFile.HasStagedChanges {
|
||||
red.Fprintln(filesView, gitFile.DisplayString)
|
||||
return
|
||||
}
|
||||
green.Fprint(filesView, gitFile.DisplayString[0:1])
|
||||
red.Fprint(filesView, gitFile.DisplayString[1:3])
|
||||
if gitFile.HasUnstagedChanges {
|
||||
red.Fprintln(filesView, gitFile.Name)
|
||||
} else {
|
||||
green.Fprintln(filesView, gitFile.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func catSelectedFile(g *gocui.Gui) (string, error) {
|
||||
item, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != ErrNoFiles {
|
||||
return "", err
|
||||
}
|
||||
return "", renderString(g, "main", "No file to display")
|
||||
}
|
||||
cat, err := catFile(item.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
func refreshFiles(g *gocui.Gui) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refreshStateGitFiles()
|
||||
filesView.Clear()
|
||||
for _, gitFile := range state.GitFiles {
|
||||
renderGitFile(gitFile, filesView)
|
||||
}
|
||||
correctCursor(filesView)
|
||||
if filesView == g.CurrentView() {
|
||||
handleFileSelect(g, filesView)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
devLog("pulling...")
|
||||
createMessagePanel(g, v, "", "Pulling...")
|
||||
go func() {
|
||||
if output, err := gitPull(); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
} else {
|
||||
closeConfirmationPrompt(g)
|
||||
refreshCommits(g)
|
||||
refreshStatus(g)
|
||||
devLog("pulled.")
|
||||
}
|
||||
refreshFiles(g)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
devLog("pushing...")
|
||||
createMessagePanel(g, v, "", "Pushing...")
|
||||
go func() {
|
||||
if output, err := gitPush(); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
} else {
|
||||
closeConfirmationPrompt(g)
|
||||
refreshCommits(g)
|
||||
refreshStatus(g)
|
||||
devLog("pushed.")
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
mergeView, err := g.View("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !file.HasMergeConflicts {
|
||||
return createErrorPanel(g, "This file has no merge conflicts")
|
||||
}
|
||||
switchFocus(g, v, mergeView)
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
output, err := gitAbortMerge()
|
||||
if err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
createMessagePanel(g, v, "", "Merge aborted")
|
||||
refreshStatus(g)
|
||||
return refreshFiles(g)
|
||||
}
|
||||
646
gitcommands.go
646
gitcommands.go
@@ -1,646 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoCheckedOutBranch : When we have no checked out branch
|
||||
ErrNoCheckedOutBranch = errors.New("No currently checked out branch")
|
||||
|
||||
// ErrNoOpenCommand : When we don't know which command to use to open a file
|
||||
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
|
||||
)
|
||||
|
||||
// GitFile : A staged/unstaged file
|
||||
// TODO: decide whether to give all of these the Git prefix
|
||||
type GitFile struct {
|
||||
Name string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// Branch : A git branch
|
||||
type Branch struct {
|
||||
Name string
|
||||
Type string
|
||||
BaseBranch string
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Pushed bool
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// Map (from https://gobyexample.com/collection-functions)
|
||||
func Map(vs []string, f func(string) string) []string {
|
||||
vsm := make([]string, len(vs))
|
||||
for i, v := range vs {
|
||||
vsm[i] = f(v)
|
||||
}
|
||||
return vsm
|
||||
}
|
||||
|
||||
func includesString(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// not sure how to genericise this because []interface{} doesn't accept e.g.
|
||||
// []int arguments
|
||||
func includesInt(list []int, a int) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
|
||||
if len(oldGitFiles) == 0 {
|
||||
return newGitFiles
|
||||
}
|
||||
|
||||
appendedIndexes := make([]int, 0)
|
||||
|
||||
// retain position of files we already could see
|
||||
result := make([]GitFile, 0)
|
||||
for _, oldGitFile := range oldGitFiles {
|
||||
for newIndex, newGitFile := range newGitFiles {
|
||||
if oldGitFile.Name == newGitFile.Name {
|
||||
result = append(result, newGitFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append any new files to the end
|
||||
for index, newGitFile := range newGitFiles {
|
||||
if !includesInt(appendedIndexes, index) {
|
||||
result = append(result, newGitFile)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func platformShell() (string, string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "cmd", "/c"
|
||||
}
|
||||
return "bash", "-c"
|
||||
}
|
||||
|
||||
func runDirectCommand(command string) (string, error) {
|
||||
timeStart := time.Now()
|
||||
commandLog(command)
|
||||
|
||||
shell, shellArg := platformShell()
|
||||
cmdOut, err := exec.
|
||||
Command(shell, shellArg, command).
|
||||
CombinedOutput()
|
||||
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart))
|
||||
return sanitisedCommandOutput(cmdOut, err)
|
||||
}
|
||||
|
||||
func branchStringParts(branchString string) (string, string) {
|
||||
// expect string to be something like '4w master`
|
||||
splitBranchName := strings.Split(branchString, "\t")
|
||||
// if we have no \t then we have no recency, so just output that as blank
|
||||
if len(splitBranchName) == 1 {
|
||||
return "", branchString
|
||||
}
|
||||
return splitBranchName[0], splitBranchName[1]
|
||||
}
|
||||
|
||||
// branchPropertiesFromName : returns branch type, base, and color
|
||||
func branchPropertiesFromName(name string) (string, string, color.Attribute) {
|
||||
if strings.Contains(name, "feature/") {
|
||||
return "feature", "develop", color.FgGreen
|
||||
} else if strings.Contains(name, "bugfix/") {
|
||||
return "bugfix", "develop", color.FgYellow
|
||||
} else if strings.Contains(name, "hotfix/") {
|
||||
return "hotfix", "master", color.FgRed
|
||||
}
|
||||
return "other", name, color.FgWhite
|
||||
}
|
||||
|
||||
func coloredString(str string, colour *color.Color) string {
|
||||
return colour.SprintFunc()(fmt.Sprint(str))
|
||||
}
|
||||
|
||||
func withPadding(str string, padding int) string {
|
||||
if padding-len(str) < 0 {
|
||||
return str
|
||||
}
|
||||
return str + strings.Repeat(" ", padding-len(str))
|
||||
}
|
||||
|
||||
// TODO: DRY up this function and getGitBranches
|
||||
func getGitStashEntries() []StashEntry {
|
||||
stashEntries := make([]StashEntry, 0)
|
||||
rawString, _ := runDirectCommand("git stash list --pretty='%gs'")
|
||||
for i, line := range splitLines(rawString) {
|
||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) StashEntry {
|
||||
return StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
DisplayString: line,
|
||||
}
|
||||
}
|
||||
|
||||
func getStashEntryDiff(index int) (string, error) {
|
||||
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
|
||||
}
|
||||
|
||||
func includes(array []string, str string) bool {
|
||||
for _, arrayStr := range array {
|
||||
if arrayStr == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getGitStatusFiles() []GitFile {
|
||||
statusOutput, _ := getGitStatus()
|
||||
statusStrings := splitLines(statusOutput)
|
||||
gitFiles := make([]GitFile, 0)
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := statusString[3:]
|
||||
tracked := !includes([]string{"??", "A "}, change)
|
||||
gitFile := GitFile{
|
||||
Name: filename,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: tracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
HasMergeConflicts: change == "UU",
|
||||
}
|
||||
devLog("tracked", gitFile.Tracked)
|
||||
devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges)
|
||||
devLog("HasStagedChanges", gitFile.HasStagedChanges)
|
||||
devLog("DisplayString", gitFile.DisplayString)
|
||||
gitFiles = append(gitFiles, gitFile)
|
||||
}
|
||||
devLog(gitFiles)
|
||||
return gitFiles
|
||||
}
|
||||
|
||||
func gitStashDo(index int, method string) (string, error) {
|
||||
return runCommand("git stash " + method + " stash@{" + fmt.Sprint(index) + "}")
|
||||
}
|
||||
|
||||
func gitStashSave(message string) (string, error) {
|
||||
output, err := runCommand("git stash save \"" + message + "\"")
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
// if there are no local changes to save, the exit code is 0, but we want
|
||||
// to raise an error
|
||||
if output == "No local changes to save\n" {
|
||||
return output, errors.New(output)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func gitCheckout(branch string, force bool) (string, error) {
|
||||
forceArg := ""
|
||||
if force {
|
||||
forceArg = "--force "
|
||||
}
|
||||
return runCommand("git checkout " + forceArg + branch)
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if outputString == "" && err != nil {
|
||||
return err.Error(), err
|
||||
}
|
||||
return outputString, err
|
||||
}
|
||||
|
||||
func runCommand(command string) (string, error) {
|
||||
commandStartTime := time.Now()
|
||||
commandLog(command)
|
||||
splitCmd := strings.Split(command, " ")
|
||||
devLog(splitCmd)
|
||||
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
|
||||
devLog("run command time: ", time.Now().Sub(commandStartTime))
|
||||
return sanitisedCommandOutput(cmdOut, err)
|
||||
}
|
||||
|
||||
func vsCodeOpenFile(g *gocui.Gui, filename string) (string, error) {
|
||||
return runCommand("code -r " + filename)
|
||||
}
|
||||
|
||||
func sublimeOpenFile(g *gocui.Gui, filename string) (string, error) {
|
||||
return runCommand("subl " + filename)
|
||||
}
|
||||
|
||||
func openFile(g *gocui.Gui, filename string) (string, error) {
|
||||
cmdName, cmdTrail, err := getOpenCommand()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return runCommand(cmdName + " " + filename + cmdTrail)
|
||||
}
|
||||
|
||||
func getOpenCommand() (string, string, error) {
|
||||
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
|
||||
trailMap := map[string]string{
|
||||
"xdg-open": " &>/dev/null &",
|
||||
"cygstart": "",
|
||||
"open": "",
|
||||
}
|
||||
for name, trail := range trailMap {
|
||||
if out, _ := runCommand("which " + name); out != "exit status 1" {
|
||||
return name, trail, nil
|
||||
}
|
||||
}
|
||||
return "", "", ErrNoOpenCommand
|
||||
}
|
||||
|
||||
func gitAddPatch(g *gocui.Gui, filename string) {
|
||||
runSubProcess(g, "git", "add", "--patch", filename)
|
||||
}
|
||||
|
||||
func editFile(g *gocui.Gui, filename string) (string, error) {
|
||||
editor, _ := gitconfig.Global("core.editor")
|
||||
if editor == "" {
|
||||
editor = os.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = os.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
|
||||
}
|
||||
runSubProcess(g, editor, filename)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
|
||||
subprocess = exec.Command(cmdName, commandArgs...)
|
||||
subprocess.Stdin = os.Stdin
|
||||
subprocess.Stdout = os.Stdout
|
||||
subprocess.Stderr = os.Stderr
|
||||
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
return ErrSubprocess
|
||||
})
|
||||
}
|
||||
|
||||
func getBranchGraph(branch string, baseBranch string) (string, error) {
|
||||
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
|
||||
|
||||
// Leaving this guy commented out in case there's backlash from the design
|
||||
// change and I want to make this configurable
|
||||
// return runCommand("git log -p -30 --color --no-merges " + branch)
|
||||
}
|
||||
|
||||
func verifyInGitRepo() {
|
||||
if output, err := runCommand("git status"); err != nil {
|
||||
fmt.Println(output)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getCommits() []Commit {
|
||||
pushables := gitCommitsToPush()
|
||||
log := getLog()
|
||||
commits := make([]Commit, 0)
|
||||
// now we can split it up and turn it into commits
|
||||
lines := splitLines(log)
|
||||
for _, line := range lines {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
pushed := includesString(pushables, sha)
|
||||
commits = append(commits, Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Pushed: pushed,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
})
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
func getLog() string {
|
||||
// currently limiting to 30 for performance reasons
|
||||
// TODO: add lazyloading when you scroll down
|
||||
result, err := runDirectCommand("git log --oneline -30")
|
||||
if err != nil {
|
||||
// assume if there is an error there are no commits yet for this branch
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func gitIgnore(filename string) {
|
||||
if _, err := runDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func gitShow(sha string) string {
|
||||
result, err := runDirectCommand("git show --color " + sha)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getDiff(file GitFile) string {
|
||||
cachedArg := ""
|
||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||
cachedArg = "--cached "
|
||||
}
|
||||
deletedArg := ""
|
||||
if file.Deleted {
|
||||
deletedArg = "-- "
|
||||
}
|
||||
trackedArg := ""
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
trackedArg = "--no-index /dev/null "
|
||||
}
|
||||
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := runCommand(command)
|
||||
return s
|
||||
}
|
||||
|
||||
func catFile(file string) (string, error) {
|
||||
return runDirectCommand("cat " + file)
|
||||
}
|
||||
|
||||
func stageFile(file string) error {
|
||||
_, err := runCommand("git add " + file)
|
||||
return err
|
||||
}
|
||||
|
||||
func unStageFile(file string, tracked bool) error {
|
||||
var command string
|
||||
if tracked {
|
||||
command = "git reset HEAD "
|
||||
} else {
|
||||
command = "git rm --cached "
|
||||
}
|
||||
devLog(command)
|
||||
_, err := runCommand(command + file)
|
||||
return err
|
||||
}
|
||||
|
||||
func getGitStatus() (string, error) {
|
||||
return runCommand("git status --untracked-files=all --short")
|
||||
}
|
||||
|
||||
func isInMergeState() (bool, error) {
|
||||
output, err := runCommand("git status --untracked-files=all")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||
}
|
||||
|
||||
func removeFile(file GitFile) error {
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if !file.Tracked {
|
||||
_, err := runCommand("rm -rf ./" + file.Name)
|
||||
return err
|
||||
}
|
||||
// if the file is tracked, we assume you want to just check it out
|
||||
_, err := runCommand("git checkout " + file.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func gitCommit(g *gocui.Gui, message string) (string, error) {
|
||||
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
||||
if gpgsign != "" {
|
||||
runSubProcess(g, "bash", "-c", "git commit -m \""+message+"\"")
|
||||
return "", nil
|
||||
}
|
||||
return runDirectCommand("git commit -m \"" + message + "\"")
|
||||
}
|
||||
|
||||
func gitPull() (string, error) {
|
||||
return runDirectCommand("git pull --no-edit")
|
||||
}
|
||||
|
||||
func gitPush() (string, error) {
|
||||
branchName := gitCurrentBranchName()
|
||||
if branchName == "" {
|
||||
return "", ErrNoCheckedOutBranch
|
||||
}
|
||||
return runDirectCommand("git push -u origin " + branchName)
|
||||
}
|
||||
|
||||
func gitSquashPreviousTwoCommits(message string) (string, error) {
|
||||
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
|
||||
}
|
||||
|
||||
func gitRenameCommit(message string) (string, error) {
|
||||
return runDirectCommand("git commit --allow-empty --amend -m \"" + message + "\"")
|
||||
}
|
||||
|
||||
func gitFetch() (string, error) {
|
||||
return runDirectCommand("git fetch")
|
||||
}
|
||||
|
||||
func gitResetToCommit(sha string) (string, error) {
|
||||
return runDirectCommand("git reset " + sha)
|
||||
}
|
||||
|
||||
func gitNewBranch(name string) (string, error) {
|
||||
return runDirectCommand("git checkout -b " + name)
|
||||
}
|
||||
|
||||
func gitListStash() (string, error) {
|
||||
return runDirectCommand("git stash list")
|
||||
}
|
||||
|
||||
func gitMerge(branchName string) (string, error) {
|
||||
return runDirectCommand("git merge --no-edit " + branchName)
|
||||
}
|
||||
|
||||
func gitAbortMerge() (string, error) {
|
||||
return runDirectCommand("git merge --abort")
|
||||
}
|
||||
|
||||
func gitUpstreamDifferenceCount() (string, string) {
|
||||
pushableCount, err := runDirectCommand("git rev-list @{u}..head --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := runDirectCommand("git rev-list head..@{u} --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||
}
|
||||
|
||||
func gitCommitsToPush() []string {
|
||||
pushables, err := runDirectCommand("git rev-list @{u}..head --abbrev-commit")
|
||||
if err != nil {
|
||||
return make([]string, 0)
|
||||
}
|
||||
return splitLines(pushables)
|
||||
}
|
||||
|
||||
func gitCurrentBranchName() string {
|
||||
branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
|
||||
// if there is an error, assume there are no branches yet
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(branchName)
|
||||
}
|
||||
|
||||
// A line will have the form '10 days ago master' so we need to strip out the
|
||||
// useful information from that into timeNumber, timeUnit, and branchName
|
||||
func branchInfoFromLine(line string) (string, string, string) {
|
||||
r := regexp.MustCompile("\\|.*\\s")
|
||||
line = r.ReplaceAllString(line, " ")
|
||||
words := strings.Split(line, " ")
|
||||
return words[0], words[1], words[3]
|
||||
}
|
||||
|
||||
func abbreviatedTimeUnit(timeUnit string) string {
|
||||
r := regexp.MustCompile("s$")
|
||||
timeUnit = r.ReplaceAllString(timeUnit, "")
|
||||
timeUnitMap := map[string]string{
|
||||
"hour": "h",
|
||||
"minute": "m",
|
||||
"second": "s",
|
||||
"week": "w",
|
||||
"year": "y",
|
||||
"day": "d",
|
||||
"month": "m",
|
||||
}
|
||||
return timeUnitMap[timeUnit]
|
||||
}
|
||||
|
||||
func getBranches() []Branch {
|
||||
branches := make([]Branch, 0)
|
||||
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||
if err != nil {
|
||||
return branches
|
||||
}
|
||||
|
||||
branchLines := splitLines(rawString)
|
||||
for i, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
|
||||
if branchAlreadyStored(branchName, branches) {
|
||||
continue
|
||||
}
|
||||
|
||||
branch := constructBranch(timeNumber+timeUnit, branchName, i)
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
func constructBranch(prefix, name string, index int) Branch {
|
||||
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
|
||||
if index == 0 {
|
||||
prefix = " *"
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
displayString := withPadding(prefix, 4) + coloredString(name, colour)
|
||||
return Branch{
|
||||
Name: name,
|
||||
Type: branchType,
|
||||
BaseBranch: branchBase,
|
||||
DisplayString: displayString,
|
||||
}
|
||||
}
|
||||
|
||||
func getGitBranches() []Branch {
|
||||
// check if there are any branches
|
||||
branchCheck, _ := runCommand("git branch")
|
||||
if branchCheck == "" {
|
||||
return []Branch{constructBranch("", gitCurrentBranchName(), 0)}
|
||||
}
|
||||
branches := getBranches()
|
||||
if len(branches) == 0 {
|
||||
branches = append(branches, constructBranch("", gitCurrentBranchName(), 0))
|
||||
}
|
||||
branches = getAndMergeFetchedBranches(branches)
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchAlreadyStored(branchName string, branches []Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if existingBranch.Name == branchName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// here branches contains all the branches that we've checked out, along with
|
||||
// the recency. In this function we append the branches that are in our heads
|
||||
// directory i.e. things we've fetched but haven't necessarily checked out.
|
||||
// Worth mentioning this has nothing to do with the 'git merge' operation
|
||||
func getAndMergeFetchedBranches(branches []Branch) []Branch {
|
||||
rawString, err := runDirectCommand("git branch --sort=-committerdate --no-color")
|
||||
if err != nil {
|
||||
return branches
|
||||
}
|
||||
branchLines := splitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
line = strings.Replace(line, "* ", "", -1)
|
||||
line = strings.TrimSpace(line)
|
||||
if branchAlreadyStored(line, branches) {
|
||||
continue
|
||||
}
|
||||
branches = append(branches, constructBranch("", line, len(branches)))
|
||||
}
|
||||
return branches
|
||||
}
|
||||
62
go.mod
Normal file
62
go.mod
Normal file
@@ -0,0 +1,62 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.15.21
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/davecgh/go-spew v1.1.0
|
||||
github.com/emirpasic/gods v1.9.0
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-ini/ini v1.38.2
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
|
||||
github.com/hashicorp/go-version v1.0.0
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
|
||||
github.com/jesseduffield/gocui v0.0.0-20180905104005-2cb6e95bbbf8
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55
|
||||
github.com/magiconair/properties v1.8.0
|
||||
github.com/mattn/go-colorable v0.0.9
|
||||
github.com/mattn/go-isatty v0.0.3
|
||||
github.com/mattn/go-runewidth v0.0.2
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699
|
||||
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80
|
||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb
|
||||
github.com/pelletier/go-buffruneio v0.2.0
|
||||
github.com/pelletier/go-toml v1.2.0
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
|
||||
github.com/sirupsen/logrus v1.0.6
|
||||
github.com/spf13/afero v1.1.1
|
||||
github.com/spf13/cast v1.2.0
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834
|
||||
github.com/spf13/pflag v1.0.2
|
||||
github.com/spf13/viper v1.1.0
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/src-d/gcfg v1.3.0
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea
|
||||
github.com/tcnksm/go-gitconfig v0.1.2
|
||||
github.com/ulikunitz/xz v0.5.4
|
||||
github.com/xanzy/ssh-agent v0.2.0
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0
|
||||
golang.org/x/text v0.3.0
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.0
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714
|
||||
gopkg.in/warnings.v0 v0.1.2
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
)
|
||||
163
go.sum
Normal file
163
go.sum
Normal file
@@ -0,0 +1,163 @@
|
||||
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/aws/aws-sdk-go v1.15.21 h1:STLvc6RrpycslC1NRtTvt/YSgDkIGCTrB9K9vE5R2oQ=
|
||||
github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
|
||||
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4=
|
||||
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001 h1:MFPzqpPED05pFyGjNPJEC2sXM6EHTzFyvX+0s0JoZ48=
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001/go.mod h1:6rdJFnhkXnzGOJbvkrdv4t9nLwKcVA+tmbQeUlkIzrU=
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
|
||||
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331 h1:qio0y/sQdhbHRA3cmgczo04MaSV2zw+n46G1owvgWIk=
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331/go.mod h1:BT+PgT529opmb6mcUY+Fg0IwVRRmwqFyavEMU17GnBg=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 h1:Nrr/yUxNjXWYK0B3IqcFlYh1ICnesJDB4ogcfOVc5Ns=
|
||||
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0=
|
||||
github.com/jesseduffield/gocui v0.0.0-20180905104005-2cb6e95bbbf8 h1:YMYnpIu5HUJfx/yfIwnZhFrgWTg51FQWtvZi+PMzQm8=
|
||||
github.com/jesseduffield/gocui v0.0.0-20180905104005-2cb6e95bbbf8/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
|
||||
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55 h1:S38dC4mEwxdw/U41+97VWdbun8mTcTjwg5Ujfg8QPME=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff h1:jM4Eo4qMmmcqePS3u6X2lcEELtVuXWkWJIS/pRI3oSk=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun9knAVAR8tg/aHJEr5DgtcbqyvzacK+CDCaI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
|
||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb h1:YahEjAGkJtCrkqgVHhX6n8ZX+CZ3hDRL9fjLYugLfSs=
|
||||
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
|
||||
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co=
|
||||
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||
github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I=
|
||||
github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 h1:kJI9pPzfsULT/72wy7mxkRQZPtKWgFdCA2RTGZ4v8/E=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.1.0 h1:V7OZpY8i3C1x/pDmU0zNNlfVoDz112fSYvtWMjjS3f4=
|
||||
github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
|
||||
github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea h1:jysxIKov/4GJ33wI2aRvuIK7yBwB28E5almlgDLPRpM=
|
||||
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea/go.mod h1:Ffmqrj3nXIMIjeA4uW3Qjj0Ud9eDoTG0fu4JxyAr/tE=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU=
|
||||
github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro=
|
||||
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171 h1:vYogbvSFj2YXcjQxFHu/rASSOt9sLytpCaSkiwQ135I=
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332 h1:efGso+ep0DjyCBJPjvoz0HI6UldX4Md2F1rZFe1ir0E=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0 h1:8H8QZJ30plJyIVj60H3lr8TZGIq2Fh3Cyrs/ZNg1foU=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/ini.v1 v1.38.2 h1:dGcbywv4RufeGeiMycPT/plKB5FtmLKLnWKwBiLhUA4=
|
||||
gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.0 h1:VGbrP1EsYxtvVPEiHui+4//imr4E5MGEFLx66bQtusg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.0/go.mod h1:ZHSF0JP+7oD97194otDUCD7Ofbk63+xFcfWP5bT6h+Q=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714 h1:+wM2BGgQ1znCKBexOB4OrGVSDw8mtKNUSq3wqxZhi/k=
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
237
gui.go
237
gui.go
@@ -1,237 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// "strings"
|
||||
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
type stateType struct {
|
||||
GitFiles []GitFile
|
||||
Branches []Branch
|
||||
Commits []Commit
|
||||
StashEntries []StashEntry
|
||||
PreviousView string
|
||||
HasMergeConflicts bool
|
||||
ConflictIndex int
|
||||
ConflictTop bool
|
||||
Conflicts []conflict
|
||||
EditHistory *stack.Stack
|
||||
}
|
||||
|
||||
type conflict struct {
|
||||
start int
|
||||
middle int
|
||||
end int
|
||||
}
|
||||
|
||||
var state = stateType{
|
||||
GitFiles: make([]GitFile, 0),
|
||||
PreviousView: "files",
|
||||
Commits: make([]Commit, 0),
|
||||
StashEntries: make([]StashEntry, 0),
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: make([]conflict, 0),
|
||||
EditHistory: stack.New(),
|
||||
}
|
||||
|
||||
func scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
ox, oy := mainView.Origin()
|
||||
if oy >= 1 {
|
||||
return mainView.SetOrigin(ox, oy-1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
ox, oy := mainView.Origin()
|
||||
if oy < len(mainView.BufferLines()) {
|
||||
return mainView.SetOrigin(ox, oy+1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
return refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func layout(g *gocui.Gui) error {
|
||||
g.Highlight = true
|
||||
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
|
||||
width, height := g.Size()
|
||||
leftSideWidth := width / 3
|
||||
statusFilesBoundary := 2
|
||||
filesBranchesBoundary := 2 * height / 5 // height - 20
|
||||
commitsBranchesBoundary := 3 * height / 5 // height - 10
|
||||
commitsStashBoundary := height - 5 // height - 5
|
||||
minimumHeight := 16
|
||||
|
||||
panelSpacing := 1
|
||||
if OverlappingEdges {
|
||||
panelSpacing = 0
|
||||
}
|
||||
|
||||
if height < minimumHeight {
|
||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Not enough space to render panels"
|
||||
v.Wrap = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
g.DeleteView("limit")
|
||||
|
||||
optionsTop := height - 2
|
||||
// hiding options if there's not enough space
|
||||
if height < 30 {
|
||||
optionsTop = height - 1
|
||||
}
|
||||
|
||||
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Diff"
|
||||
v.Wrap = true
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Status"
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
filesView.Highlight = true
|
||||
filesView.Title = "Files"
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Branches"
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Commits"
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = "Stash"
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.BgColor = gocui.ColorDefault
|
||||
v.FgColor = gocui.ColorBlue
|
||||
v.Frame = false
|
||||
}
|
||||
|
||||
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.BgColor = gocui.ColorDefault
|
||||
v.FgColor = gocui.ColorGreen
|
||||
v.Frame = false
|
||||
renderString(g, "version", version)
|
||||
|
||||
// these are only called once
|
||||
handleFileSelect(g, filesView)
|
||||
refreshFiles(g)
|
||||
refreshBranches(g)
|
||||
refreshCommits(g)
|
||||
refreshStashEntries(g)
|
||||
nextView(g, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetch(g *gocui.Gui) {
|
||||
gitFetch()
|
||||
refreshStatus(g)
|
||||
}
|
||||
|
||||
func updateLoader(g *gocui.Gui) {
|
||||
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
||||
content := trimmedContent(confirmationView)
|
||||
if strings.Contains(content, "...") {
|
||||
staticContent := strings.Split(content, "...")[0] + "..."
|
||||
renderString(g, "confirmation", staticContent+" "+loader())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func run() (err error) {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer g.Close()
|
||||
|
||||
// periodically fetching to check for upstream differences
|
||||
go func() {
|
||||
for range time.Tick(time.Second * 60) {
|
||||
fetch(g)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for range time.Tick(time.Millisecond * 10) {
|
||||
updateLoader(g)
|
||||
}
|
||||
}()
|
||||
|
||||
g.SetManagerFunc(layout)
|
||||
|
||||
if err = keybindings(g); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = g.MainLoop()
|
||||
return
|
||||
}
|
||||
|
||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
|
||||
// is only handled if the given view has focus, or handled globally if the view
|
||||
// is ""
|
||||
type Binding struct {
|
||||
ViewName string
|
||||
Handler func(*gocui.Gui, *gocui.View) error
|
||||
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
|
||||
Modifier gocui.Modifier
|
||||
}
|
||||
|
||||
func keybindings(g *gocui.Gui) error {
|
||||
bindings := []Binding{
|
||||
Binding{ViewName: "", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
|
||||
Binding{ViewName: "", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
|
||||
Binding{ViewName: "", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
|
||||
Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
|
||||
Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
|
||||
Binding{ViewName: "", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
|
||||
Binding{ViewName: "", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
|
||||
Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
||||
Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
||||
Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
|
||||
Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
|
||||
Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
|
||||
Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
|
||||
Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
|
||||
Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
|
||||
Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
|
||||
Binding{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
|
||||
Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
|
||||
Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
|
||||
Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
|
||||
Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
|
||||
Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
|
||||
Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
|
||||
Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
|
||||
Binding{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
|
||||
Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
|
||||
Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
||||
Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
|
||||
Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
|
||||
Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
|
||||
Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
||||
Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
||||
Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
|
||||
Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
|
||||
Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
|
||||
Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
|
||||
Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
|
||||
Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
|
||||
Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
|
||||
Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
|
||||
Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
|
||||
Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
|
||||
Binding{ViewName: "stash", Key: 'k', Modifier: gocui.ModNone, Handler: handleStashPop},
|
||||
Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
98
main.go
98
main.go
@@ -1,93 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"time"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// ErrSubProcess is raised when we are running a subprocess
|
||||
var (
|
||||
ErrSubprocess = errors.New("running subprocess")
|
||||
subprocess *exec.Cmd
|
||||
startTime time.Time
|
||||
commit string
|
||||
version = "unversioned"
|
||||
date string
|
||||
buildSource = "unknown"
|
||||
|
||||
commit string
|
||||
version = "unversioned"
|
||||
|
||||
date string
|
||||
configFlag = flag.Bool("config", false, "Print the current default config")
|
||||
debuggingFlag = flag.Bool("debug", false, "a boolean")
|
||||
versionFlag = flag.Bool("v", false, "Print the current version")
|
||||
)
|
||||
|
||||
func homeDirectory() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return usr.HomeDir
|
||||
}
|
||||
|
||||
func devLog(objects ...interface{}) {
|
||||
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
|
||||
}
|
||||
|
||||
func colorLog(colour color.Attribute, objects ...interface{}) {
|
||||
localLog(colour, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/development.log", objects...)
|
||||
}
|
||||
|
||||
func commandLog(objects ...interface{}) {
|
||||
localLog(color.FgWhite, homeDirectory()+"/go/src/github.com/jesseduffield/lazygit/commands.log", objects...)
|
||||
}
|
||||
|
||||
func localLog(colour color.Attribute, path string, objects ...interface{}) {
|
||||
if !*debuggingFlag {
|
||||
return
|
||||
}
|
||||
f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
defer f.Close()
|
||||
for _, object := range objects {
|
||||
colorFunction := color.New(colour).SprintFunc()
|
||||
f.WriteString(colorFunction(fmt.Sprint(object)) + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory() {
|
||||
_, err := os.Stat(".git")
|
||||
for os.IsNotExist(err) {
|
||||
devLog("going up a directory to find the root")
|
||||
os.Chdir("..")
|
||||
_, err = os.Stat(".git")
|
||||
}
|
||||
func projectPath(path string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
||||
}
|
||||
|
||||
func main() {
|
||||
startTime = time.Now()
|
||||
devLog("\n\n\n\n\n\n\n\n\n\n")
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
fmt.Printf("commit=%s, build date=%s, version=%s", commit, date, version)
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
verifyInGitRepo()
|
||||
navigateToRepoRootDirectory()
|
||||
for {
|
||||
if err := run(); err != nil {
|
||||
if err == gocui.ErrQuit {
|
||||
break
|
||||
} else if err == ErrSubprocess {
|
||||
subprocess.Run()
|
||||
} else {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
if *configFlag {
|
||||
fmt.Printf("%s\n", config.GetDefaultConfig())
|
||||
os.Exit(0)
|
||||
}
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app, err := app.Setup(appConfig)
|
||||
if err != nil {
|
||||
app.Log.Error(err.Error())
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app.Gui.RunWithSubprocesses()
|
||||
}
|
||||
|
||||
264
merge_panel.go
264
merge_panel.go
@@ -1,264 +0,0 @@
|
||||
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func findConflicts(content string) ([]conflict, error) {
|
||||
conflicts := make([]conflict, 0)
|
||||
var newConflict conflict
|
||||
for i, line := range splitLines(content) {
|
||||
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
||||
newConflict = conflict{start: i}
|
||||
} else if line == "=======" {
|
||||
newConflict.middle = i
|
||||
} else if strings.HasPrefix(line, ">>>>>>> ") {
|
||||
newConflict.end = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
return conflicts, nil
|
||||
}
|
||||
|
||||
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
|
||||
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
||||
}
|
||||
|
||||
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
||||
if len(conflicts) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
conflict, remainingConflicts := shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range splitLines(content) {
|
||||
colourAttr := color.FgWhite
|
||||
if i == conflict.start || i == conflict.middle || i == conflict.end {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
}
|
||||
if i == conflict.end && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(coloredString(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String(), nil
|
||||
}
|
||||
|
||||
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||
state.ConflictTop = true
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
state.ConflictTop = false
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
if state.ConflictIndex >= len(state.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
state.ConflictIndex++
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
if state.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
state.ConflictIndex--
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func isIndexToDelete(i int, conflict conflict, pick string) bool {
|
||||
return i == conflict.middle ||
|
||||
i == conflict.start ||
|
||||
i == conflict.end ||
|
||||
pick != "both" &&
|
||||
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
|
||||
(pick == "top" && i > conflict.middle && i < conflict.end)
|
||||
}
|
||||
|
||||
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
|
||||
gitFile, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
output := ""
|
||||
for i := 0; true; i++ {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !isIndexToDelete(i, conflict, pick) {
|
||||
output += line
|
||||
}
|
||||
}
|
||||
devLog(output)
|
||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
}
|
||||
|
||||
func pushFileSnapshot(g *gocui.Gui) error {
|
||||
gitFile, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := catFile(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.EditHistory.Push(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
colorLog(color.FgCyan, "IM HERE")
|
||||
if state.EditHistory.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
prevContent := state.EditHistory.Pop().(string)
|
||||
gitFile, err := getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
conflict := state.Conflicts[state.ConflictIndex]
|
||||
pushFileSnapshot(g)
|
||||
pick := "bottom"
|
||||
if state.ConflictTop {
|
||||
pick = "top"
|
||||
}
|
||||
err := resolveConflict(g, conflict, pick)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
refreshMergePanel(g)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||
conflict := state.Conflicts[state.ConflictIndex]
|
||||
pushFileSnapshot(g)
|
||||
err := resolveConflict(g, conflict, "both")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func currentViewName(g *gocui.Gui) string {
|
||||
currentView := g.CurrentView()
|
||||
return currentView.Name()
|
||||
}
|
||||
|
||||
func refreshMergePanel(g *gocui.Gui) error {
|
||||
cat, err := catSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state.Conflicts, err = findConflicts(cat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(state.Conflicts) == 0 {
|
||||
return handleCompleteMerge(g)
|
||||
} else if state.ConflictIndex > len(state.Conflicts)-1 {
|
||||
state.ConflictIndex = len(state.Conflicts) - 1
|
||||
}
|
||||
hasFocus := currentViewName(g) == "main"
|
||||
if hasFocus {
|
||||
renderMergeOptions(g)
|
||||
}
|
||||
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := scrollToConflict(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return renderString(g, "main", content)
|
||||
}
|
||||
|
||||
func scrollToConflict(g *gocui.Gui) error {
|
||||
mainView, err := g.View("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(state.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
conflict := state.Conflicts[state.ConflictIndex]
|
||||
ox, _ := mainView.Origin()
|
||||
_, height := mainView.Size()
|
||||
conflictMiddle := (conflict.end + conflict.start) / 2
|
||||
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
||||
return mainView.SetOrigin(ox, newOriginY)
|
||||
}
|
||||
|
||||
func switchToMerging(g *gocui.Gui) error {
|
||||
state.ConflictIndex = 0
|
||||
state.ConflictTop = true
|
||||
_, err := g.SetCurrentView("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func renderMergeOptions(g *gocui.Gui) error {
|
||||
return renderOptionsMap(g, map[string]string{
|
||||
"↑ ↓": "select hunk",
|
||||
"← →": "navigate conflicts",
|
||||
"space": "pick hunk",
|
||||
"b": "pick both hunks",
|
||||
"z": "undo",
|
||||
})
|
||||
}
|
||||
|
||||
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refreshFiles(g)
|
||||
return switchFocus(g, v, filesView)
|
||||
}
|
||||
|
||||
func handleCompleteMerge(g *gocui.Gui) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stageSelectedFile(g)
|
||||
refreshFiles(g)
|
||||
return switchFocus(g, nil, filesView)
|
||||
}
|
||||
104
pkg/app/app.go
Normal file
104
pkg/app/app.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/heroku/rollrus"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
closers []io.Closer
|
||||
|
||||
Config config.AppConfigurer
|
||||
Log *logrus.Entry
|
||||
OSCommand *commands.OSCommand
|
||||
GitCommand *commands.GitCommand
|
||||
Gui *gui.Gui
|
||||
Tr *i18n.Localizer
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
return log
|
||||
}
|
||||
|
||||
func newDevelopmentLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
|
||||
}
|
||||
log.SetOutput(file)
|
||||
return log
|
||||
}
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
environment := "production"
|
||||
if config.GetDebug() {
|
||||
environment = "development"
|
||||
log = newDevelopmentLogger()
|
||||
} else {
|
||||
log = newProductionLogger(config)
|
||||
}
|
||||
if config.GetUserConfig().GetString("reporting") == "on" {
|
||||
// this isn't really a secret token: it only has permission to push new rollbar items
|
||||
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)
|
||||
log.Hooks.Add(hook)
|
||||
}
|
||||
return log.WithFields(logrus.Fields{
|
||||
"debug": config.GetDebug(),
|
||||
"version": config.GetVersion(),
|
||||
"commit": config.GetCommit(),
|
||||
"buildDate": config.GetBuildDate(),
|
||||
})
|
||||
}
|
||||
|
||||
// Setup bootstrap a new application
|
||||
func Setup(config config.AppConfigurer) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
||||
|
||||
app.Tr = i18n.NewLocalizer(app.Log)
|
||||
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// Close closes any resources
|
||||
func (app *App) Close() error {
|
||||
for _, closer := range app.closers {
|
||||
err := closer.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
39
pkg/commands/branch.go
Normal file
39
pkg/commands/branch.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Branch : A git branch
|
||||
// duplicating this for now
|
||||
type Branch struct {
|
||||
Name string
|
||||
Recency string
|
||||
}
|
||||
|
||||
// GetDisplayString returns the dispaly string of branch
|
||||
func (b *Branch) GetDisplayString() string {
|
||||
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
|
||||
}
|
||||
|
||||
// GetColor branch color
|
||||
func (b *Branch) GetColor() color.Attribute {
|
||||
switch b.getType() {
|
||||
case "feature":
|
||||
return color.FgGreen
|
||||
case "bugfix":
|
||||
return color.FgYellow
|
||||
case "hotfix":
|
||||
return color.FgRed
|
||||
default:
|
||||
return color.FgWhite
|
||||
}
|
||||
}
|
||||
|
||||
// expected to return feature/bugfix/hotfix or blank string
|
||||
func (b *Branch) getType() string {
|
||||
return strings.Split(b.Name, "/")[0]
|
||||
}
|
||||
524
pkg/commands/git.go
Normal file
524
pkg/commands/git.go
Normal file
@@ -0,0 +1,524 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
func verifyInGitRepo(runCmd func(string) error) error {
|
||||
return runCmd("git status")
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
for {
|
||||
f, err := stat(".git")
|
||||
|
||||
if err == nil && f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = chdir(".."); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
|
||||
repository, err = openGitRepository(".")
|
||||
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
|
||||
return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
worktree, err = repository.Worktree()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer) (*GitCommand, error) {
|
||||
var worktree *gogit.Worktree
|
||||
var repo *gogit.Repository
|
||||
|
||||
fs := []func() error{
|
||||
func() error {
|
||||
return verifyInGitRepo(osCommand.RunCommand)
|
||||
},
|
||||
func() error {
|
||||
return navigateToRepoRootDirectory(os.Stat, os.Chdir)
|
||||
},
|
||||
func() error {
|
||||
var err error
|
||||
repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if err := f(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Worktree: worktree,
|
||||
Repo: repo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entryies
|
||||
func (c *GitCommand) GetStashEntries() []StashEntry {
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
|
||||
stashEntries := []StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) StashEntry {
|
||||
return StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
DisplayString: line,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
|
||||
}
|
||||
|
||||
// GetStatusFiles git status files
|
||||
func (c *GitCommand) GetStatusFiles() []File {
|
||||
statusOutput, _ := c.GitStatus()
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
|
||||
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
|
||||
|
||||
file := File{
|
||||
Name: filename,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
HasMergeConflicts: change == "UU",
|
||||
Type: c.OSCommand.FileType(filename),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
c.Log.Info(files) // TODO: use a dumper-esque log here
|
||||
return files
|
||||
}
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
}
|
||||
|
||||
headResults := []File{}
|
||||
tailResults := []File{}
|
||||
|
||||
for _, newFile := range newFiles {
|
||||
var isHeadResult bool
|
||||
|
||||
for _, oldFile := range oldFiles {
|
||||
if oldFile.Name == newFile.Name {
|
||||
isHeadResult = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isHeadResult {
|
||||
headResults = append(headResults, newFile)
|
||||
continue
|
||||
}
|
||||
|
||||
tailResults = append(tailResults, newFile)
|
||||
}
|
||||
|
||||
return append(headResults, tailResults...)
|
||||
}
|
||||
|
||||
// GetBranchName branch name
|
||||
func (c *GitCommand) GetBranchName() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
}
|
||||
|
||||
// ResetHard does the equivalent of `git reset --hard HEAD`
|
||||
func (c *GitCommand) ResetHard() error {
|
||||
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
|
||||
}
|
||||
|
||||
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
|
||||
// current branch
|
||||
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
|
||||
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list head..@{u} --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||
}
|
||||
|
||||
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
|
||||
// to the remote branch of the current branch
|
||||
func (c *GitCommand) GetCommitsToPush() []string {
|
||||
pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
return utils.SplitLines(pushables)
|
||||
}
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
|
||||
}
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch() error {
|
||||
return c.OSCommand.RunCommand("git fetch")
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
|
||||
}
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
|
||||
}
|
||||
|
||||
// DeleteBranch delete branch
|
||||
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
||||
command := "git branch -d"
|
||||
|
||||
if force {
|
||||
command = "git branch -D"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
|
||||
}
|
||||
|
||||
// ListStash list stash
|
||||
func (c *GitCommand) ListStash() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash list")
|
||||
}
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.OSCommand.RunCommand("git merge --abort")
|
||||
}
|
||||
|
||||
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) UsingGpg() bool {
|
||||
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = gitconfig.Local("commit.gpgsign")
|
||||
}
|
||||
if gpgsign == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Commit commit to git
|
||||
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) {
|
||||
command := "git commit -m " + c.OSCommand.Quote(message)
|
||||
if c.UsingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
}
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Pull pull from repo
|
||||
func (c *GitCommand) Pull() error {
|
||||
return c.OSCommand.RunCommand("git pull --no-edit")
|
||||
}
|
||||
|
||||
// Push push to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease "
|
||||
}
|
||||
return c.OSCommand.RunCommand("git push " + forceFlag + "-u origin " + branchName)
|
||||
}
|
||||
|
||||
// SquashPreviousTwoCommits squashes a commit down to the one below it
|
||||
// retaining the message of the higher commit
|
||||
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
|
||||
// TODO: test this
|
||||
err := c.OSCommand.RunCommand("git reset --soft HEAD^")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: if password is required, we need to return a subprocess
|
||||
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message))
|
||||
}
|
||||
|
||||
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
|
||||
// retaining the commit message of the lower commit
|
||||
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
|
||||
var err error
|
||||
commands := []string{
|
||||
"git checkout -q " + shaValue,
|
||||
"git reset --soft " + shaValue + "^",
|
||||
"git commit --amend -C " + shaValue + "^",
|
||||
"git rebase --onto HEAD " + shaValue + " " + branchName,
|
||||
}
|
||||
ret := ""
|
||||
for _, command := range commands {
|
||||
c.Log.Info(command)
|
||||
output, err := c.OSCommand.RunCommandWithOutput(command)
|
||||
ret += output
|
||||
if err != nil {
|
||||
c.Log.Info(ret)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// We are already in an error state here so we're just going to append
|
||||
// the output of these commands
|
||||
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue)
|
||||
ret += output
|
||||
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName)
|
||||
ret += output
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New(ret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CatFile obtain the contents of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("cat " + c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.OSCommand.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
// UnstageAll stages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.OSCommand.RunCommand("git reset")
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
var command string
|
||||
if tracked {
|
||||
command = "git reset HEAD "
|
||||
} else {
|
||||
command = "git rm --cached "
|
||||
}
|
||||
return c.OSCommand.RunCommand(command + c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
func (c *GitCommand) GitStatus() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||
}
|
||||
|
||||
// RemoveFile directly
|
||||
func (c *GitCommand) RemoveFile(file File) error {
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges {
|
||||
if err := c.OSCommand.RunCommand("git reset -- " + file.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !file.Tracked {
|
||||
return os.RemoveAll(file.Name)
|
||||
}
|
||||
// if the file is tracked, we assume you want to just check it out
|
||||
return c.OSCommand.RunCommand("git checkout -- " + file.Name)
|
||||
}
|
||||
|
||||
// Checkout checks out a branch, with --force if you set the force arg to true
|
||||
func (c *GitCommand) Checkout(branch string, force bool) error {
|
||||
forceArg := ""
|
||||
if force {
|
||||
forceArg = "--force "
|
||||
}
|
||||
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
|
||||
}
|
||||
|
||||
// AddPatch prepares a subprocess for adding a patch by patch
|
||||
// this will eventually be swapped out for a better solution inside the Gui
|
||||
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
|
||||
}
|
||||
|
||||
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||
func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||
}
|
||||
|
||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
// Currently it limits the result to 100 commits, but when we get async stuff
|
||||
// working we can do lazy loading
|
||||
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branchName)
|
||||
}
|
||||
|
||||
func includesString(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *GitCommand) GetCommits() []Commit {
|
||||
pushables := c.GetCommitsToPush()
|
||||
log := c.GetLog()
|
||||
commits := []Commit{}
|
||||
// now we can split it up and turn it into commits
|
||||
lines := utils.SplitLines(log)
|
||||
for _, line := range lines {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
pushed := includesString(pushables, sha)
|
||||
commits = append(commits, Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Pushed: pushed,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
})
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
// GetLog gets the git log (currently limited to 30 commits for performance
|
||||
// until we work out lazy loading
|
||||
func (c *GitCommand) GetLog() string {
|
||||
// currently limiting to 30 for performance reasons
|
||||
// TODO: add lazyloading when you scroll down
|
||||
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
|
||||
if err != nil {
|
||||
// assume if there is an error there are no commits yet for this branch
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
func (c *GitCommand) Ignore(filename string) error {
|
||||
return c.OSCommand.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
// Show shows the diff of a commit
|
||||
func (c *GitCommand) Show(sha string) string {
|
||||
result, err := c.OSCommand.RunCommandWithOutput("git show --color " + sha)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file File) string {
|
||||
cachedArg := ""
|
||||
fileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
trackedArg := "--"
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
}
|
||||
command := fmt.Sprintf("%s %s %s %s", "git diff --color ", cachedArg, trackedArg, fileName)
|
||||
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(command)
|
||||
return s
|
||||
}
|
||||
36
pkg/commands/git_structs.go
Normal file
36
pkg/commands/git_structs.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
// File : A staged/unstaged file
|
||||
type File struct {
|
||||
Name string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
DisplayString string
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
}
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Pushed bool
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
826
pkg/commands/git_test.go
Normal file
826
pkg/commands/git_test.go
Normal file
@@ -0,0 +1,826 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
type fileInfoMock struct {
|
||||
name string
|
||||
size int64
|
||||
fileMode os.FileMode
|
||||
fileModTime time.Time
|
||||
isDir bool
|
||||
sys interface{}
|
||||
}
|
||||
|
||||
func (f fileInfoMock) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fileInfoMock) Size() int64 {
|
||||
return f.size
|
||||
}
|
||||
|
||||
func (f fileInfoMock) Mode() os.FileMode {
|
||||
return f.fileMode
|
||||
}
|
||||
|
||||
func (f fileInfoMock) ModTime() time.Time {
|
||||
return f.fileModTime
|
||||
}
|
||||
|
||||
func (f fileInfoMock) IsDir() bool {
|
||||
return f.isDir
|
||||
}
|
||||
|
||||
func (f fileInfoMock) Sys() interface{} {
|
||||
return f.sys
|
||||
}
|
||||
|
||||
func newDummyLog() *logrus.Entry {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
return log.WithField("test", "test")
|
||||
}
|
||||
|
||||
func newDummyGitCommand() *GitCommand {
|
||||
return &GitCommand{
|
||||
Log: newDummyLog(),
|
||||
OSCommand: newDummyOSCommand(),
|
||||
Tr: i18n.NewLocalizer(newDummyLog()),
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyInGitRepo(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runCmd func(string) error
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Valid git repository",
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Not a valid git repository",
|
||||
func(string) error {
|
||||
return fmt.Errorf("fatal: Not a git repository (or any of the parent directories): .git")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(verifyInGitRepo(s.runCmd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNavigateToRepoRootDirectory(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
stat func(string) (os.FileInfo, error)
|
||||
chdir func(string) error
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Navigate to git repository",
|
||||
func(string) (os.FileInfo, error) {
|
||||
return fileInfoMock{isDir: true}, nil
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when getting path informations",
|
||||
func(string) (os.FileInfo, error) {
|
||||
return nil, fmt.Errorf("An error occurred")
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "An error occurred")
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when trying to move one path backward",
|
||||
func(string) (os.FileInfo, error) {
|
||||
return nil, os.ErrNotExist
|
||||
},
|
||||
func(string) error {
|
||||
return fmt.Errorf("An error occurred")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "An error occurred")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(navigateToRepoRootDirectory(s.stat, s.chdir))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupRepositoryAndWorktree(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
openGitRepository func(string) (*gogit.Repository, error)
|
||||
sLocalize func(string) string
|
||||
test func(*gogit.Repository, *gogit.Worktree, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"A gitconfig parsing error occurred",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
|
||||
},
|
||||
func(string) string {
|
||||
return "error translated"
|
||||
},
|
||||
func(r *gogit.Repository, w *gogit.Worktree, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "error translated")
|
||||
},
|
||||
},
|
||||
{
|
||||
"A gogit error occurred",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
return nil, fmt.Errorf("Error from inside gogit")
|
||||
},
|
||||
func(string) string { return "" },
|
||||
func(r *gogit.Repository, w *gogit.Worktree, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "Error from inside gogit")
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred cause git repository is a bare repository",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
return &gogit.Repository{}, nil
|
||||
},
|
||||
func(string) string { return "" },
|
||||
func(r *gogit.Repository, w *gogit.Worktree, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gogit.ErrIsBareRepository, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Setup done properly",
|
||||
func(string) (*gogit.Repository, error) {
|
||||
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
|
||||
r, err := gogit.PlainInit("/tmp/lazygit-test", false)
|
||||
assert.NoError(t, err)
|
||||
return r, nil
|
||||
},
|
||||
func(string) string { return "" },
|
||||
func(r *gogit.Repository, w *gogit.Worktree, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, w)
|
||||
assert.NotNil(t, r)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(setupRepositoryAndWorktree(s.openGitRepository, s.sLocalize))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewGitCommand(t *testing.T) {
|
||||
actual, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
assert.NoError(t, os.Chdir(actual))
|
||||
}()
|
||||
|
||||
type scenario struct {
|
||||
testName string
|
||||
setup func()
|
||||
test func(*GitCommand, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred, folder doesn't contains a git repository",
|
||||
func() {
|
||||
assert.NoError(t, os.Chdir("/tmp"))
|
||||
},
|
||||
func(gitCmd *GitCommand, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Regexp(t, "fatal: .ot a git repository \\(or any of the parent directories\\): \\.git", err.Error())
|
||||
},
|
||||
},
|
||||
{
|
||||
"New GitCommand object created",
|
||||
func() {
|
||||
assert.NoError(t, os.RemoveAll("/tmp/lazygit-test"))
|
||||
_, err := gogit.PlainInit("/tmp/lazygit-test", false)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, os.Chdir("/tmp/lazygit-test"))
|
||||
},
|
||||
func(gitCmd *GitCommand, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.setup()
|
||||
s.test(NewGitCommand(newDummyLog(), newDummyOSCommand(), i18n.NewLocalizer(newDummyLog())))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]StashEntry)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(entries []StashEntry) {
|
||||
assert.Len(t, entries, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
|
||||
},
|
||||
func(entries []StashEntry) {
|
||||
expected := []StashEntry{
|
||||
{
|
||||
0,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
1,
|
||||
"WIP on master: bb86a3f update github template",
|
||||
"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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandGetStashEntryDiff(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", "show", "-p", "--color", "stash@{1}"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
_, err := gitCmd.GetStashEntryDiff(1)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(files []File) {
|
||||
assert.Len(t, files, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command(
|
||||
"echo",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt",
|
||||
)
|
||||
},
|
||||
func(files []File) {
|
||||
assert.Len(t, files, 4)
|
||||
|
||||
expected := []File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashDo(1, "drop"))
|
||||
}
|
||||
|
||||
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 exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashSave("A stash message"))
|
||||
}
|
||||
|
||||
func TestGitCommandCommitAmend(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", "--amend", "--allow-empty"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
_, err := gitCmd.PrepareCommitAmendSubProcess().CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitCommandMergeStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
oldFiles []File
|
||||
newFiles []File
|
||||
test func([]File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Old file and new file are the same",
|
||||
[]File{},
|
||||
[]File{
|
||||
{
|
||||
Name: "new_file.txt",
|
||||
},
|
||||
},
|
||||
func(files []File) {
|
||||
expected := []File{
|
||||
{
|
||||
Name: "new_file.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, files, 1)
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files to merge, with some identical",
|
||||
[]File{
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file2.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file3.txt",
|
||||
},
|
||||
},
|
||||
[]File{
|
||||
{
|
||||
Name: "new_file4.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file5.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
},
|
||||
func(files []File) {
|
||||
expected := []File{
|
||||
{
|
||||
Name: "new_file1.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file4.txt",
|
||||
},
|
||||
{
|
||||
Name: "new_file5.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, files, 3)
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := newDummyGitCommand()
|
||||
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandUpstreamDifferentCount(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 exec.Command("exit", "1")
|
||||
},
|
||||
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 exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
return exec.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 exec.Command("echo", "10")
|
||||
}
|
||||
|
||||
return exec.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.UpstreamDifferenceCount())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandGetCommitsToPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Can't retrieve pushable commits",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(pushables []string) {
|
||||
assert.EqualValues(t, []string{}, pushables)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Retrieve pushable commits",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "8a2bb0e\n78976bc")
|
||||
},
|
||||
func(pushables []string) {
|
||||
assert.Len(t, pushables, 2)
|
||||
assert.EqualValues(t, []string{"8a2bb0e", "78976bc"}, pushables)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := newDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.GetCommitsToPush())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandRenameCommit(t *testing.T) {
|
||||
gitCmd := newDummyGitCommand()
|
||||
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.RenameCommit("test"))
|
||||
}
|
||||
|
||||
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", "78976bc"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc"))
|
||||
}
|
||||
|
||||
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"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test"))
|
||||
}
|
||||
|
||||
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 exec.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 exec.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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test"))
|
||||
}
|
||||
|
||||
func TestGitCommandDiff(t *testing.T) {
|
||||
gitCommand := newDummyGitCommand()
|
||||
assert.NoError(t, test.GenerateRepo("lots_of_diffs.sh"))
|
||||
|
||||
files := []File{
|
||||
{
|
||||
Name: "deleted_staged",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: true,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: " D deleted_staged",
|
||||
},
|
||||
{
|
||||
Name: "file with space staged",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "A \"file with space staged\"",
|
||||
},
|
||||
{
|
||||
Name: "file with space unstaged",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "?? file with space unstaged",
|
||||
},
|
||||
{
|
||||
Name: "modified_unstaged",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "M modified_unstaged",
|
||||
},
|
||||
{
|
||||
Name: "modified_staged",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: " M modified_staged",
|
||||
},
|
||||
{
|
||||
Name: "renamed_before -> renamed_after",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "R renamed_before -> renamed_after",
|
||||
},
|
||||
{
|
||||
Name: "untracked_unstaged",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "?? untracked_unstaged",
|
||||
},
|
||||
{
|
||||
Name: "untracked_staged",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "A untracked_staged",
|
||||
},
|
||||
{
|
||||
Name: "master",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "?? master",
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t.Run(file.Name, func(t *testing.T) {
|
||||
assert.NotContains(t, gitCommand.Diff(file), "error")
|
||||
})
|
||||
}
|
||||
}
|
||||
167
pkg/commands/os.go
Normal file
167
pkg/commands/os.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
os string
|
||||
shell string
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
openCommand string
|
||||
fallbackEscapedQuote string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
type OSCommand struct {
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getenv func(string) string
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
return &OSCommand{
|
||||
Log: log,
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
command: exec.Command,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getenv: os.Getenv,
|
||||
}
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
splitCmd := str.ToArgv(command)
|
||||
c.Log.Info(splitCmd)
|
||||
return sanitisedCommandOutput(
|
||||
c.command(splitCmd[0], splitCmd[1:]...).CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
// RunCommand runs a command and just returns the error
|
||||
func (c *OSCommand) RunCommand(command string) error {
|
||||
_, err := c.RunCommandWithOutput(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "other"
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return "directory"
|
||||
}
|
||||
return "file"
|
||||
}
|
||||
|
||||
// RunDirectCommand wrapper around direct commands
|
||||
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||
|
||||
return sanitisedCommandOutput(
|
||||
c.command(c.Platform.shell, c.Platform.shellArg, command).
|
||||
CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", err
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor, _ := c.getGlobalGitConfig("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.RunCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return c.PrepareSubProcess(editor, filename), nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
return c.command(cmdName, commandArgs...)
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.escapedQuote
|
||||
if strings.Contains(message, c.Platform.escapedQuote) {
|
||||
escapedQuote = c.Platform.fallbackEscapedQuote
|
||||
}
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
return err
|
||||
}
|
||||
18
pkg/commands/os_default_platform.go
Normal file
18
pkg/commands/os_default_platform.go
Normal file
@@ -0,0 +1,18 @@
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: runtime.GOOS,
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
openCommand: "open {{filename}}",
|
||||
fallbackEscapedQuote: "\"",
|
||||
}
|
||||
}
|
||||
359
pkg/commands/os_test.go
Normal file
359
pkg/commands/os_test.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func newDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(newDummyLog(), newDummyAppConfig())
|
||||
}
|
||||
|
||||
func newDummyAppConfig() *config.AppConfig {
|
||||
appConfig := &config.AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
Commit: "",
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: viper.New(),
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
}
|
||||
|
||||
func TestOSCommandRunCommandWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(newDummyOSCommand().RunCommandWithOutput(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandRunCommand(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(newDummyOSCommand().RunCommand(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
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 exec.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 exec.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 exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := newDummyOSCommand()
|
||||
OSCmd.command = s.command
|
||||
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandEditFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
test func(*exec.Cmd, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "emacs", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "vi", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := newDummyOSCommand()
|
||||
OSCmd.command = s.command
|
||||
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
|
||||
OSCmd.getenv = s.getenv
|
||||
|
||||
s.test(OSCmd.EditFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
|
||||
func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
|
||||
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
|
||||
expected := "hello test"
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
setup func()
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"testFile",
|
||||
func() {
|
||||
if _, err := os.Create("testFile"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file with spaces",
|
||||
func() {
|
||||
if _, err := os.Create("file with spaces"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"testDirectory",
|
||||
func() {
|
||||
if err := os.Mkdir("testDirectory", 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "directory", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"nonExistant",
|
||||
func() {},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "other", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.setup()
|
||||
s.test(newDummyOSCommand().FileType(s.path))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
11
pkg/commands/os_windows.go
Normal file
11
pkg/commands/os_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package commands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: "windows",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: `\"`,
|
||||
fallbackEscapedQuote: "\\'",
|
||||
}
|
||||
}
|
||||
256
pkg/config/app_config.go
Normal file
256
pkg/config/app_config.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/shibukawa/configdir"
|
||||
"github.com/spf13/viper"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// AppConfig contains the base configuration fields required for lazygit.
|
||||
type AppConfig struct {
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *viper.Viper
|
||||
AppState *AppState
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
// from AppConfig and still be used by lazygit.
|
||||
type AppConfigurer interface {
|
||||
GetDebug() bool
|
||||
GetVersion() string
|
||||
GetCommit() string
|
||||
GetBuildDate() string
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
GetUserConfig() *viper.Viper
|
||||
GetAppState() *AppState
|
||||
WriteToUserConfig(string, string) error
|
||||
SaveAppState() error
|
||||
LoadAppState() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) {
|
||||
userConfig, err := LoadConfig("config", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: *debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
AppState: &AppState{},
|
||||
}
|
||||
|
||||
if err := appConfig.LoadAppState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
// GetDebug returns debug flag
|
||||
func (c *AppConfig) GetDebug() bool {
|
||||
return c.Debug
|
||||
}
|
||||
|
||||
// GetVersion returns debug flag
|
||||
func (c *AppConfig) GetVersion() string {
|
||||
return c.Version
|
||||
}
|
||||
|
||||
// GetCommit returns debug flag
|
||||
func (c *AppConfig) GetCommit() string {
|
||||
return c.Commit
|
||||
}
|
||||
|
||||
// GetBuildDate returns debug flag
|
||||
func (c *AppConfig) GetBuildDate() string {
|
||||
return c.BuildDate
|
||||
}
|
||||
|
||||
// GetName returns debug flag
|
||||
func (c *AppConfig) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
// GetBuildSource returns the source of the build. For builds from goreleaser
|
||||
// this will be binaryBuild
|
||||
func (c *AppConfig) GetBuildSource() string {
|
||||
return c.BuildSource
|
||||
}
|
||||
|
||||
// GetUserConfig returns the user config
|
||||
func (c *AppConfig) GetUserConfig() *viper.Viper {
|
||||
return c.UserConfig
|
||||
}
|
||||
|
||||
// GetAppState returns the app state
|
||||
func (c *AppConfig) GetAppState() *AppState {
|
||||
return c.AppState
|
||||
}
|
||||
|
||||
func newViper(filename string) (*viper.Viper, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
v.SetConfigName(filename)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// LoadConfig gets the user's config
|
||||
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) {
|
||||
v, err := newViper(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withDefaults {
|
||||
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = LoadAndMergeFile(v, filename+".yml"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// LoadDefaults loads in the defaults defined in this file
|
||||
func LoadDefaults(v *viper.Viper, defaults []byte) error {
|
||||
return v.MergeConfig(bytes.NewBuffer(defaults))
|
||||
}
|
||||
|
||||
func prepareConfigFile(filename string) (string, error) {
|
||||
// chucking my name there is not for vanity purposes, the xdg spec (and that
|
||||
// function) requires a vendor name. May as well line up with github
|
||||
configDirs := configdir.New("jesseduffield", "lazygit")
|
||||
folder := configDirs.QueryFolderContainsFile(filename)
|
||||
if folder == nil {
|
||||
// create the file as empty
|
||||
folders := configDirs.QueryFolders(configdir.Global)
|
||||
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
folder = configDirs.QueryFolderContainsFile(filename)
|
||||
}
|
||||
return filepath.Join(folder.Path, filename), nil
|
||||
}
|
||||
|
||||
// LoadAndMergeFile Loads the config/state file, creating
|
||||
// the file as an empty one if it does not exist
|
||||
func LoadAndMergeFile(v *viper.Viper, filename string) error {
|
||||
configPath, err := prepareConfigFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.AddConfigPath(filepath.Dir(configPath))
|
||||
return v.MergeInConfig()
|
||||
}
|
||||
|
||||
// WriteToUserConfig adds a key/value pair to the user's config and saves it
|
||||
func (c *AppConfig) WriteToUserConfig(key, value string) error {
|
||||
// reloading the user config directly (without defaults) so that we're not
|
||||
// writing any defaults back to the user's config
|
||||
v, err := LoadConfig("config", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.Set(key, value)
|
||||
return v.WriteConfig()
|
||||
}
|
||||
|
||||
// SaveAppState marhsalls the AppState struct and writes it to the disk
|
||||
func (c *AppConfig) SaveAppState() error {
|
||||
marshalledAppState, err := yaml.Marshal(c.AppState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filepath, err := prepareConfigFile("state.yml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
}
|
||||
|
||||
// LoadAppState loads recorded AppState from file
|
||||
func (c *AppConfig) LoadAppState() error {
|
||||
filepath, err := prepareConfigFile("state.yml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appStateBytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appStateBytes) == 0 {
|
||||
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
|
||||
}
|
||||
return yaml.Unmarshal(appStateBytes, c.AppState)
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns the application default configuration
|
||||
func GetDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`gui:
|
||||
## stuff relating to the UI
|
||||
scrollHeight: 2
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often a update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
`)
|
||||
}
|
||||
|
||||
// AppState stores data between runs of the app like when the last update check
|
||||
// was performed and which other repos have been checked out
|
||||
type AppState struct {
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
}
|
||||
|
||||
func getDefaultAppState() []byte {
|
||||
return []byte(`
|
||||
lastUpdateCheck: 0
|
||||
recentRepos: []
|
||||
`)
|
||||
}
|
||||
|
||||
// // commenting this out until we use it again
|
||||
// func homeDirectory() string {
|
||||
// usr, err := user.Current()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// return usr.HomeDir
|
||||
// }
|
||||
10
pkg/config/config_default_platform.go
Normal file
10
pkg/config/config_default_platform.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// +build !windows,!linux
|
||||
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'open {{filename}}'`)
|
||||
}
|
||||
8
pkg/config/config_linux.go
Normal file
8
pkg/config/config_linux.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'`)
|
||||
}
|
||||
8
pkg/config/config_windows.go
Normal file
8
pkg/config/config_windows.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'`)
|
||||
}
|
||||
163
pkg/git/branch_list_builder.go
Normal file
163
pkg/git/branch_list_builder.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
)
|
||||
|
||||
// context:
|
||||
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
||||
// which `git branch -a` gives us, but we also want the recency data that
|
||||
// git reflog gives us.
|
||||
// So we get the HEAD, then append get the reflog branches that intersect with
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
|
||||
// I used go-git for this, but that breaks if you've just done a git init,
|
||||
// even though you're on 'master'
|
||||
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err != nil {
|
||||
branchName, err = b.GitCommand.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
|
||||
branches := make([]commands.Branch, 0)
|
||||
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||
if err != nil {
|
||||
return branches
|
||||
}
|
||||
|
||||
branchLines := utils.SplitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
|
||||
branches := make([]commands.Branch, 0)
|
||||
|
||||
bIter, err := b.GitCommand.Repo.Branches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
name := b.Name().Short()
|
||||
branches = append(branches, commands.Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
|
||||
for _, newBranch := range newBranches {
|
||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||
finalBranches = append(finalBranches, newBranch)
|
||||
}
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
|
||||
for _, safeBranch := range safeBranches {
|
||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||
return safeBranch.Name
|
||||
}
|
||||
}
|
||||
return reflogBranch.Name
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []commands.Branch {
|
||||
branches := make([]commands.Branch, 0)
|
||||
head := b.obtainCurrentBranch()
|
||||
safeBranches := b.obtainSafeBranches()
|
||||
if len(safeBranches) == 0 {
|
||||
return append(branches, head)
|
||||
}
|
||||
reflogBranches := b.obtainReflogBranches()
|
||||
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
|
||||
for i, reflogBranch := range reflogBranches {
|
||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||
}
|
||||
|
||||
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []commands.Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueByName(branches []commands.Branch) []commands.Branch {
|
||||
finalBranches := make([]commands.Branch, 0)
|
||||
for _, branch := range branches {
|
||||
if branchIncluded(branch.Name, finalBranches) {
|
||||
continue
|
||||
}
|
||||
finalBranches = append(finalBranches, branch)
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
// A line will have the form '10 days ago master' so we need to strip out the
|
||||
// useful information from that into timeNumber, timeUnit, and branchName
|
||||
func branchInfoFromLine(line string) (string, string, string) {
|
||||
r := regexp.MustCompile("\\|.*\\s")
|
||||
line = r.ReplaceAllString(line, " ")
|
||||
words := strings.Split(line, " ")
|
||||
return words[0], words[1], words[len(words)-1]
|
||||
}
|
||||
|
||||
func abbreviatedTimeUnit(timeUnit string) string {
|
||||
r := regexp.MustCompile("s$")
|
||||
timeUnit = r.ReplaceAllString(timeUnit, "")
|
||||
timeUnitMap := map[string]string{
|
||||
"hour": "h",
|
||||
"minute": "m",
|
||||
"second": "s",
|
||||
"week": "w",
|
||||
"year": "y",
|
||||
"day": "d",
|
||||
"month": "m",
|
||||
}
|
||||
return timeUnitMap[timeUnit]
|
||||
}
|
||||
44
pkg/gui/app_status_manager.go
Normal file
44
pkg/gui/app_status_manager.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
type appStatus struct {
|
||||
name string
|
||||
statusType string
|
||||
duration int
|
||||
}
|
||||
|
||||
type statusManager struct {
|
||||
statuses []appStatus
|
||||
}
|
||||
|
||||
func (m *statusManager) removeStatus(name string) {
|
||||
newStatuses := []appStatus{}
|
||||
for _, status := range m.statuses {
|
||||
if status.name != name {
|
||||
newStatuses = append(newStatuses, status)
|
||||
}
|
||||
}
|
||||
m.statuses = newStatuses
|
||||
}
|
||||
|
||||
func (m *statusManager) addWaitingStatus(name string) {
|
||||
m.removeStatus(name)
|
||||
newStatus := appStatus{
|
||||
name: name,
|
||||
statusType: "waiting",
|
||||
duration: 0,
|
||||
}
|
||||
m.statuses = append([]appStatus{newStatus}, m.statuses...)
|
||||
}
|
||||
|
||||
func (m *statusManager) getStatusString() string {
|
||||
if len(m.statuses) == 0 {
|
||||
return ""
|
||||
}
|
||||
topStatus := m.statuses[0]
|
||||
if topStatus.statusType == "waiting" {
|
||||
return topStatus.name + " " + utils.Loader()
|
||||
}
|
||||
return topStatus.name
|
||||
}
|
||||
162
pkg/gui/branches_panel.go
Normal file
162
pkg/gui/branches_panel.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
index := gui.getItemPosition(gui.getBranchesView(g))
|
||||
if index == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
}
|
||||
branch := gui.getSelectedBranch(gui.getBranchesView(g))
|
||||
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch(v)
|
||||
message := gui.Tr.SLocalize("SureForceCheckout")
|
||||
title := gui.Tr.SLocalize("ForceCheckoutBranch")
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.State.Branches[0]
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"NewBranchNameBranchOff",
|
||||
Teml{
|
||||
"branchName": branch.Name,
|
||||
},
|
||||
)
|
||||
gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.refreshSidePanels(g)
|
||||
return gui.handleBranchSelect(g, v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.deleteBranch(g, v, false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.deleteBranch(g, v, true)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
checkedOutBranch := gui.State.Branches[0]
|
||||
selectedBranch := gui.getSelectedBranch(v)
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
}
|
||||
title := gui.Tr.SLocalize("DeleteBranch")
|
||||
var messageId string
|
||||
if force {
|
||||
messageId = "ForceDeleteBranchMessage"
|
||||
} else {
|
||||
messageId = "DeleteBranchMessage"
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
messageId,
|
||||
Teml{
|
||||
"selectedBranchName": selectedBranch.Name,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
checkedOutBranch := gui.State.Branches[0]
|
||||
selectedBranch := gui.getSelectedBranch(v)
|
||||
defer gui.refreshSidePanels(g)
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
|
||||
}
|
||||
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
|
||||
lineNumber := gui.getItemPosition(v)
|
||||
return gui.State.Branches[lineNumber]
|
||||
}
|
||||
|
||||
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
|
||||
return gui.renderGlobalOptions(g)
|
||||
}
|
||||
|
||||
// may want to standardise how these select methods work
|
||||
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.renderBranchesOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
// This really shouldn't happen: there should always be a master branch
|
||||
if len(gui.State.Branches) == 0 {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
|
||||
}
|
||||
go func() {
|
||||
branch := gui.getSelectedBranch(v)
|
||||
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
|
||||
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
||||
diff = gui.Tr.SLocalize("NoTrackingThisBranch")
|
||||
}
|
||||
gui.renderString(g, "main", diff)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// gui.refreshStatus is called at the end of this because that's when we can
|
||||
// be sure there is a state.Branches array to pick the current branch from
|
||||
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
v, err := g.View("branches")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
v.Clear()
|
||||
for _, branch := range gui.State.Branches {
|
||||
fmt.Fprintln(v, branch.GetDisplayString())
|
||||
}
|
||||
gui.resetOrigin(v)
|
||||
return gui.refreshStatus(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
87
pkg/gui/commit_message_panel.go
Normal file
87
pkg/gui/commit_message_panel.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
|
||||
}
|
||||
sub, err := gui.GitCommand.Commit(g, message)
|
||||
if err != nil {
|
||||
// TODO need to find a way to send through this error
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return gui.Errors.ErrSubProcess
|
||||
}
|
||||
gui.refreshFiles(g)
|
||||
v.Clear()
|
||||
v.SetCursor(0, 0)
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
gui.switchFocus(g, v, gui.getFilesView(g))
|
||||
return gui.refreshCommits(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
return gui.switchFocus(g, v, gui.getFilesView(g))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
},
|
||||
)
|
||||
return gui.renderString(g, "options", message)
|
||||
}
|
||||
|
||||
func (gui *Gui) simpleEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == gocui.KeyTab:
|
||||
v.EditNewLine()
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
default:
|
||||
v.EditWrite(ch)
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
}
|
||||
|
||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
|
||||
}
|
||||
|
||||
func (gui *Gui) RenderCommitLength() {
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
|
||||
return
|
||||
}
|
||||
v := gui.getCommitMessageView(gui.g)
|
||||
v.Subtitle = gui.getBufferLength(v)
|
||||
}
|
||||
179
pkg/gui/commits_panel.go
Normal file
179
pkg/gui/commits_panel.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
gui.State.Commits = gui.GitCommand.GetCommits()
|
||||
v, err := g.View("commits")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v.Clear()
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
white := color.New(color.FgWhite)
|
||||
shaColor := white
|
||||
for _, commit := range gui.State.Commits {
|
||||
if commit.Pushed {
|
||||
shaColor = red
|
||||
} else {
|
||||
shaColor = yellow
|
||||
}
|
||||
shaColor.Fprint(v, commit.Sha+" ")
|
||||
white.Fprintln(v, commit.Name)
|
||||
}
|
||||
gui.refreshStatus(g)
|
||||
if g.CurrentView().Name() == "commits" {
|
||||
gui.handleCommitSelect(g, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
commit, err := gui.getSelectedCommit(g)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := gui.refreshFiles(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.resetOrigin(commitView)
|
||||
return gui.handleCommitSelect(g, nil)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
|
||||
return gui.renderGlobalOptions(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.renderCommitsOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
commit, err := gui.getSelectedCommit(g)
|
||||
if err != nil {
|
||||
if err.Error() != gui.Tr.SLocalize("NoCommitsThisBranch") {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
commitText := gui.GitCommand.Show(commit.Sha)
|
||||
return gui.renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.getItemPosition(v) != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
|
||||
}
|
||||
if len(gui.State.Commits) == 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
commit, err := gui.getSelectedCommit(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.refreshStatus(g)
|
||||
return gui.handleCommitSelect(g, v)
|
||||
}
|
||||
|
||||
// TODO: move to files panel
|
||||
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
|
||||
for _, file := range files {
|
||||
if file.Tracked && file.HasUnstagedChanges {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
if len(gui.State.Commits) == 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
if gui.anyUnStagedChanges(gui.State.Files) {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
|
||||
}
|
||||
branch := gui.State.Branches[0]
|
||||
commit, err := gui.getSelectedCommit(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := gui.Tr.SLocalize("SureFixupThisCommit")
|
||||
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.refreshStatus(g)
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
|
||||
}
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.handleCommitSelect(g, v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
|
||||
}
|
||||
|
||||
gui.SubProcess = gui.GitCommand.PrepareCommitAmendSubProcess()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
|
||||
v, err := g.View("commits")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(gui.State.Commits) == 0 {
|
||||
return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
lineNumber := gui.getItemPosition(v)
|
||||
if lineNumber > len(gui.State.Commits)-1 {
|
||||
gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber)
|
||||
return gui.State.Commits[len(gui.State.Commits)-1], nil
|
||||
}
|
||||
return gui.State.Commits[lineNumber], nil
|
||||
}
|
||||
145
pkg/gui/confirmation_panel.go
Normal file
145
pkg/gui/confirmation_panel.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// lots of this has been directly ported from one of the example files, will brush up later
|
||||
|
||||
// Copyright 2014 The gocui Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
if function != nil {
|
||||
if err := function(g, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return gui.closeConfirmationPrompt(g)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
|
||||
view, err := g.View("confirmation")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := gui.returnFocus(g, view); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.DeleteKeybindings("confirmation")
|
||||
return g.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) getMessageHeight(message string, width int) int {
|
||||
lines := strings.Split(message, "\n")
|
||||
lineCount := 0
|
||||
for _, line := range lines {
|
||||
lineCount += len(line)/width + 1
|
||||
}
|
||||
return lineCount
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
||||
width, height := g.Size()
|
||||
panelWidth := width / 2
|
||||
panelHeight := gui.getMessageHeight(prompt, panelWidth)
|
||||
return width/2 - panelWidth/2,
|
||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||
width/2 + panelWidth/2,
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||
gui.onNewPopupPanel()
|
||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = true
|
||||
return gui.setKeyBindings(g, handleConfirm, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string) (*gocui.View, error) {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, prompt)
|
||||
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return nil, err
|
||||
}
|
||||
confirmationView.Title = title
|
||||
confirmationView.FgColor = gocui.ColorWhite
|
||||
}
|
||||
confirmationView.Clear()
|
||||
|
||||
if err := gui.switchFocus(gui.g, currentView, confirmationView); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return confirmationView, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewPopupPanel() {
|
||||
gui.g.SetViewOnBottom("commitMessage")
|
||||
}
|
||||
|
||||
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
gui.onNewPopupPanel()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
// delete the existing confirmation panel if it exists
|
||||
if view, _ := g.View("confirmation"); view != nil {
|
||||
if err := gui.closeConfirmationPrompt(g); err != nil {
|
||||
errMessage := gui.Tr.TemplateLocalize(
|
||||
"CantCloseConfirmationPrompt",
|
||||
Teml{
|
||||
"error": err.Error(),
|
||||
},
|
||||
)
|
||||
gui.Log.Error(errMessage)
|
||||
}
|
||||
}
|
||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = false
|
||||
if err := gui.renderString(g, "confirmation", prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.setKeyBindings(g, handleConfirm, handleClose)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
actions := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
},
|
||||
)
|
||||
if err := gui.renderString(g, "options", actions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
|
||||
}
|
||||
|
||||
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
||||
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||
gui.Log.Error(message)
|
||||
currentView := g.CurrentView()
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
|
||||
}
|
||||
418
pkg/gui/files_panel.go
Normal file
418
pkg/gui/files_panel.go
Normal file
@@ -0,0 +1,418 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) stagedFiles() []commands.File {
|
||||
files := gui.State.Files
|
||||
result := make([]commands.File, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) trackedFiles() []commands.File {
|
||||
files := gui.State.Files
|
||||
result := make([]commands.File, 0)
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.GitCommand.StageFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if file.HasMergeConflicts {
|
||||
return gui.handleSwitchToMerge(g, v)
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
gui.GitCommand.StageFile(file.Name)
|
||||
} else {
|
||||
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleFileSelect(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
var err error
|
||||
if gui.allFilesStaged() {
|
||||
err = gui.GitCommand.UnstageAll()
|
||||
} else {
|
||||
err = gui.GitCommand.StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleFileSelect(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !file.HasUnstagedChanges {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
|
||||
}
|
||||
if !file.Tracked {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
|
||||
}
|
||||
|
||||
gui.SubProcess = gui.GitCommand.AddPatch(file.Name)
|
||||
return gui.Errors.ErrSubProcess
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
|
||||
if len(gui.State.Files) == 0 {
|
||||
return commands.File{}, gui.Errors.ErrNoFiles
|
||||
}
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
lineNumber := gui.getItemPosition(filesView)
|
||||
return gui.State.Files[lineNumber], nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var deleteVerb string
|
||||
if file.Tracked {
|
||||
deleteVerb = gui.Tr.SLocalize("checkout")
|
||||
} else {
|
||||
deleteVerb = gui.Tr.SLocalize("delete")
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"SureTo",
|
||||
Teml{
|
||||
"deleteVerb": deleteVerb,
|
||||
"fileName": file.Name,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.RemoveFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshFiles(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if file.Tracked {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
|
||||
}
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshFiles(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
|
||||
return gui.renderGlobalOptions(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
|
||||
return gui.renderfilesOptions(g, nil)
|
||||
}
|
||||
gui.renderfilesOptions(g, &file)
|
||||
var content string
|
||||
if file.HasMergeConflicts {
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
content = gui.GitCommand.Diff(file)
|
||||
return gui.renderString(g, "main", content)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
}
|
||||
commitMessageView := gui.getCommitMessageView(g)
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
g.SetViewOnTop("commitMessage")
|
||||
gui.switchFocus(g, filesView, commitMessageView)
|
||||
gui.RenderCommitLength()
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
}
|
||||
gui.PrepareSubProcess(g, "git", "commit")
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
|
||||
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
|
||||
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
sub, err := gui.OSCommand.EditFile(filename)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return gui.Errors.ErrSubProcess
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshFiles(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateFiles() {
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles()
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
|
||||
gui.updateHasMergeConflictStatus()
|
||||
}
|
||||
|
||||
func (gui *Gui) updateHasMergeConflictStatus() error {
|
||||
merging, err := gui.GitCommand.IsInMergeState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.HasMergeConflicts = merging
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
red.Fprintln(filesView, file.DisplayString)
|
||||
return
|
||||
}
|
||||
green.Fprint(filesView, file.DisplayString[0:1])
|
||||
red.Fprint(filesView, file.DisplayString[1:3])
|
||||
if file.HasUnstagedChanges {
|
||||
red.Fprintln(filesView, file.Name)
|
||||
} else {
|
||||
green.Fprintln(filesView, file.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||
item, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return "", err
|
||||
}
|
||||
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
|
||||
}
|
||||
if item.Type != "file" {
|
||||
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
|
||||
}
|
||||
cat, err := gui.GitCommand.CatFile(item.Name)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return "", gui.renderString(g, "main", err.Error())
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.refreshStateFiles()
|
||||
filesView.Clear()
|
||||
for _, file := range gui.State.Files {
|
||||
gui.renderFile(file, filesView)
|
||||
}
|
||||
gui.correctCursor(filesView)
|
||||
if filesView == g.CurrentView() {
|
||||
gui.handleFileSelect(g, filesView)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait"))
|
||||
go func() {
|
||||
if err := gui.GitCommand.Pull(); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
} else {
|
||||
gui.closeConfirmationPrompt(g)
|
||||
gui.refreshCommits(g)
|
||||
gui.refreshStatus(g)
|
||||
}
|
||||
gui.refreshFiles(g)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(currentView *gocui.View, force bool) error {
|
||||
if err := gui.createMessagePanel(gui.g, currentView, "", gui.Tr.SLocalize("PushWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
branchName := gui.State.Branches[0].Name
|
||||
if err := gui.GitCommand.Push(branchName, force); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(gui.g)
|
||||
_ = gui.refreshCommits(gui.g)
|
||||
_ = gui.refreshStatus(gui.g)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
_, pullables := gui.GitCommand.UpstreamDifferenceCount()
|
||||
if pullables == "?" || pullables == "0" {
|
||||
return gui.pushWithForceFlag(v, false)
|
||||
}
|
||||
err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(v, true)
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
mergeView, err := g.View("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
}
|
||||
gui.switchFocus(g, v, mergeView)
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.AbortMerge(); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
|
||||
gui.refreshStatus(g)
|
||||
return gui.refreshFiles(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.ResetHard(); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshFiles(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
430
pkg/gui/gui.go
Normal file
430
pkg/gui/gui.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// "strings"
|
||||
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
// SentinelErrors are the errors that have special meaning and need to be checked
|
||||
// by calling functions. The less of these, the better
|
||||
type SentinelErrors struct {
|
||||
ErrSubProcess error
|
||||
ErrNoFiles error
|
||||
}
|
||||
|
||||
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
|
||||
// because we can't do package-scoped errors with localization, and also because
|
||||
// it seems like package-scoped variables are bad in general
|
||||
// https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables
|
||||
// In the future it would be good to implement some of the recommendations of
|
||||
// that article. For now, if we don't need an error to be a sentinel, we will just
|
||||
// define it inline. This has implications for error messages that pop up everywhere
|
||||
// in that we'll be duplicating the default values. We may need to look at
|
||||
// having a default localisation bundle defined, and just using keys-only when
|
||||
// localising things in the code.
|
||||
func (gui *Gui) GenerateSentinelErrors() {
|
||||
gui.Errors = SentinelErrors{
|
||||
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
|
||||
ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")),
|
||||
}
|
||||
}
|
||||
|
||||
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
|
||||
type Teml i18n.Teml
|
||||
|
||||
// Gui wraps the gocui Gui object which handles rendering and events
|
||||
type Gui struct {
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *commands.OSCommand
|
||||
SubProcess *exec.Cmd
|
||||
State guiState
|
||||
Config config.AppConfigurer
|
||||
Tr *i18n.Localizer
|
||||
Errors SentinelErrors
|
||||
Updater *updates.Updater
|
||||
statusManager *statusManager
|
||||
}
|
||||
|
||||
type guiState struct {
|
||||
Files []commands.File
|
||||
Branches []commands.Branch
|
||||
Commits []commands.Commit
|
||||
StashEntries []commands.StashEntry
|
||||
PreviousView string
|
||||
HasMergeConflicts bool
|
||||
ConflictIndex int
|
||||
ConflictTop bool
|
||||
Conflicts []commands.Conflict
|
||||
EditHistory *stack.Stack
|
||||
Platform commands.Platform
|
||||
Updating bool
|
||||
Keys []Binding
|
||||
}
|
||||
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
|
||||
|
||||
initialState := guiState{
|
||||
Files: make([]commands.File, 0),
|
||||
PreviousView: "files",
|
||||
Commits: make([]commands.Commit, 0),
|
||||
StashEntries: make([]commands.StashEntry, 0),
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: make([]commands.Conflict, 0),
|
||||
EditHistory: stack.New(),
|
||||
Platform: *oSCommand.Platform,
|
||||
}
|
||||
|
||||
gui := &Gui{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
State: initialState,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
}
|
||||
|
||||
gui.GenerateSentinelErrors()
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
ox, oy := mainView.Origin()
|
||||
if oy >= 1 {
|
||||
return mainView.SetOrigin(ox, oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
ox, oy := mainView.Origin()
|
||||
if oy < len(mainView.BufferLines()) {
|
||||
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// layout is called for every screen re-render e.g. when the screen is resized
|
||||
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
g.Highlight = true
|
||||
width, height := g.Size()
|
||||
version := gui.Config.GetVersion()
|
||||
leftSideWidth := width / 3
|
||||
statusFilesBoundary := 2
|
||||
filesBranchesBoundary := 2 * height / 5 // height - 20
|
||||
commitsBranchesBoundary := 3 * height / 5 // height - 10
|
||||
commitsStashBoundary := height - 5 // height - 5
|
||||
optionsVersionBoundary := width - max(len(version), 1)
|
||||
minimumHeight := 16
|
||||
minimumWidth := 10
|
||||
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
appStatusOptionsBoundary := 0
|
||||
if appStatus != "" {
|
||||
appStatusOptionsBoundary = len(appStatus) + 2
|
||||
}
|
||||
|
||||
panelSpacing := 1
|
||||
if OverlappingEdges {
|
||||
panelSpacing = 0
|
||||
}
|
||||
|
||||
if height < minimumHeight || width < minimumWidth {
|
||||
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
|
||||
v.Wrap = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
g.DeleteView("limit")
|
||||
|
||||
optionsTop := height - 2
|
||||
// hiding options if there's not enough space
|
||||
if height < 30 {
|
||||
optionsTop = height - 1
|
||||
}
|
||||
|
||||
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
v.Wrap = true
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("StatusTitle")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
filesView.Highlight = true
|
||||
filesView.Title = gui.Tr.SLocalize("FilesTitle")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("BranchesTitle")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("CommitsTitle")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("StashTitle")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
}
|
||||
|
||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, optionsVersionBoundary-1, optionsTop+2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Frame = false
|
||||
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if gui.getCommitMessageView(g) == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width/2, height/2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
|
||||
commitMessageView.FgColor = gocui.ColorWhite
|
||||
commitMessageView.Editable = true
|
||||
commitMessageView.Editor = gocui.EditorFunc(gui.simpleEditor)
|
||||
}
|
||||
}
|
||||
|
||||
if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
appStatusView.BgColor = gocui.ColorDefault
|
||||
appStatusView.FgColor = gocui.ColorCyan
|
||||
appStatusView.Frame = false
|
||||
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if v, err := g.SetView("version", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.BgColor = gocui.ColorDefault
|
||||
v.FgColor = gocui.ColorGreen
|
||||
v.Frame = false
|
||||
if err := gui.renderString(g, "version", version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// these are only called once (it's a place to put all the things you want
|
||||
// to happen on startup after the screen is first rendered)
|
||||
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
|
||||
gui.handleFileSelect(g, filesView)
|
||||
gui.refreshFiles(g)
|
||||
gui.refreshBranches(g)
|
||||
gui.refreshCommits(g)
|
||||
gui.refreshStashEntries(g)
|
||||
if err := gui.switchFocus(g, nil, filesView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
|
||||
if err := gui.promptAnonymousReporting(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.resizeCurrentPopupPanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) promptAnonymousReporting() error {
|
||||
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.Config.WriteToUserConfig("reporting", "on")
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.Config.WriteToUserConfig("reporting", "off")
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(g *gocui.Gui) error {
|
||||
gui.GitCommand.Fetch()
|
||||
gui.refreshStatus(g)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) updateLoader(g *gocui.Gui) error {
|
||||
if view, _ := g.View("confirmation"); view != nil {
|
||||
content := gui.trimmedContent(view)
|
||||
if strings.Contains(content, "...") {
|
||||
staticContent := strings.Split(content, "...")[0] + "..."
|
||||
if err := gui.renderString(g, "confirmation", staticContent+" "+utils.Loader()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderAppStatus(g *gocui.Gui) error {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
if appStatus != "" {
|
||||
return gui.renderString(gui.g, "appStatus", appStatus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderGlobalOptions(g *gocui.Gui) error {
|
||||
return gui.renderOptionsMap(g, map[string]string{
|
||||
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
|
||||
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
|
||||
"esc/q": gui.Tr.SLocalize("close"),
|
||||
"x": gui.Tr.SLocalize("menu"),
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
|
||||
go func() {
|
||||
for range time.Tick(interval) {
|
||||
function(g)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer g.Close()
|
||||
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
|
||||
if err := gui.SetColorScheme(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.goEvery(g, time.Second*60, gui.fetch)
|
||||
gui.goEvery(g, time.Second*10, gui.refreshFiles)
|
||||
gui.goEvery(g, time.Millisecond*50, gui.updateLoader)
|
||||
gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus)
|
||||
|
||||
g.SetManagerFunc(gui.layout)
|
||||
|
||||
if err = gui.keybindings(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = g.MainLoop()
|
||||
return err
|
||||
}
|
||||
|
||||
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
|
||||
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
|
||||
// otherwise it handles the error, possibly by quitting the application
|
||||
func (gui *Gui) RunWithSubprocesses() {
|
||||
for {
|
||||
if err := gui.Run(); err != nil {
|
||||
if err == gocui.ErrQuit {
|
||||
break
|
||||
} else if err == gui.Errors.ErrSubProcess {
|
||||
gui.SubProcess.Stdin = os.Stdin
|
||||
gui.SubProcess.Stdout = os.Stdout
|
||||
gui.SubProcess.Stderr = os.Stderr
|
||||
gui.SubProcess.Run()
|
||||
gui.SubProcess.Stdout = ioutil.Discard
|
||||
gui.SubProcess.Stderr = ioutil.Discard
|
||||
gui.SubProcess.Stdin = nil
|
||||
gui.SubProcess = nil
|
||||
} else {
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation(g, v)
|
||||
}
|
||||
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
|
||||
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
}
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
388
pkg/gui/keybindings.go
Normal file
388
pkg/gui/keybindings.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
|
||||
// is only handled if the given view has focus, or handled globally if the view
|
||||
// is ""
|
||||
type Binding struct {
|
||||
ViewName string
|
||||
Handler func(*gocui.Gui, *gocui.View) error
|
||||
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
|
||||
Modifier gocui.Modifier
|
||||
KeyReadable string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (gui *Gui) GetKeybindings() []Binding {
|
||||
bindings := []Binding{
|
||||
{
|
||||
ViewName: "",
|
||||
Key: 'q',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlC,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgup,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgdn,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlU,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlD,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: 'P',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.pushFiles,
|
||||
Description: gui.Tr.SLocalize("push"),
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: 'p',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.pullFiles,
|
||||
Description: gui.Tr.SLocalize("pull"),
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: 'R',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRefresh,
|
||||
Description: gui.Tr.SLocalize("refresh"),
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: 'x',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenu,
|
||||
}, {
|
||||
ViewName: "status",
|
||||
Key: 'e',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEditConfig,
|
||||
Description: gui.Tr.SLocalize("EditConfig"),
|
||||
}, {
|
||||
ViewName: "status",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleOpenConfig,
|
||||
Description: gui.Tr.SLocalize("OpenConfig"),
|
||||
}, {
|
||||
ViewName: "status",
|
||||
Key: 'u',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCheckForUpdate,
|
||||
Description: gui.Tr.SLocalize("checkForUpdate"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'c',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitPress,
|
||||
Description: gui.Tr.SLocalize("CommitChanges"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'C',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitEditorPress,
|
||||
Description: gui.Tr.SLocalize("CommitChangesWithEditor"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFilePress,
|
||||
KeyReadable: "space",
|
||||
Description: gui.Tr.SLocalize("toggleStaged"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFileRemove,
|
||||
Description: gui.Tr.SLocalize("removeFile"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'm',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSwitchToMerge,
|
||||
Description: gui.Tr.SLocalize("resolveMergeConflicts"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'e',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFileEdit,
|
||||
Description: gui.Tr.SLocalize("editFile"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFileOpen,
|
||||
Description: gui.Tr.SLocalize("openFile"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'i',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleIgnoreFile,
|
||||
Description: gui.Tr.SLocalize("ignoreFile"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'r',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRefreshFiles,
|
||||
Description: gui.Tr.SLocalize("refreshFiles"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStashSave,
|
||||
Description: gui.Tr.SLocalize("stashFiles"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'A',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleAbortMerge,
|
||||
Description: gui.Tr.SLocalize("abortMerge"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageAll,
|
||||
Description: gui.Tr.SLocalize("toggleStagedAll"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 't',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleAddPatch,
|
||||
Description: gui.Tr.SLocalize("addPatch"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleResetHard,
|
||||
Description: gui.Tr.SLocalize("resetHard"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEscapeMerge,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'b',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickBothHunks,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'z',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePopFileSnapshot,
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleBranchPress,
|
||||
KeyReadable: "space",
|
||||
Description: gui.Tr.SLocalize("checkout"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'c',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCheckoutByName,
|
||||
Description: gui.Tr.SLocalize("checkoutByName"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'F',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleForceCheckout,
|
||||
Description: gui.Tr.SLocalize("forceCheckout"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'n',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleNewBranch,
|
||||
Description: gui.Tr.SLocalize("newBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleDeleteBranch,
|
||||
Description: gui.Tr.SLocalize("deleteBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleForceDeleteBranch,
|
||||
Description: gui.Tr.SLocalize("forceDeleteBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'm',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMerge,
|
||||
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 's',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitSquashDown,
|
||||
Description: gui.Tr.SLocalize("squashDown"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'r',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRenameCommit,
|
||||
Description: gui.Tr.SLocalize("renameCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'R',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRenameCommitEditor,
|
||||
Description: gui.Tr.SLocalize("renameCommitEditor"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'g',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleResetToCommit,
|
||||
Description: gui.Tr.SLocalize("resetToThisCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'f',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitFixup,
|
||||
Description: gui.Tr.SLocalize("fixupCommit"),
|
||||
}, {
|
||||
ViewName: "stash",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStashApply,
|
||||
KeyReadable: "space",
|
||||
Description: gui.Tr.SLocalize("apply"),
|
||||
}, {
|
||||
ViewName: "stash",
|
||||
Key: 'g',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStashPop,
|
||||
Description: gui.Tr.SLocalize("pop"),
|
||||
}, {
|
||||
ViewName: "stash",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStashDrop,
|
||||
Description: gui.Tr.SLocalize("drop"),
|
||||
}, {
|
||||
ViewName: "commitMessage",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitConfirm,
|
||||
}, {
|
||||
ViewName: "commitMessage",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitClose,
|
||||
}, {
|
||||
ViewName: "menu",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenuClose,
|
||||
}, {
|
||||
ViewName: "menu",
|
||||
Key: 'q',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenuClose,
|
||||
}, {
|
||||
ViewName: "menu",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenuPress,
|
||||
},
|
||||
}
|
||||
|
||||
// Would make these keybindings global but that interferes with editing
|
||||
// input in the confirmation panel
|
||||
for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} {
|
||||
bindings = append(bindings, []Binding{
|
||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown},
|
||||
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp},
|
||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown},
|
||||
}...)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||
bindings := gui.GetKeybindings()
|
||||
|
||||
for _, binding := range bindings {
|
||||
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
131
pkg/gui/menu_panel.go
Normal file
131
pkg/gui/menu_panel.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleMenuPress(g *gocui.Gui, v *gocui.View) error {
|
||||
lineNumber := gui.getItemPosition(v)
|
||||
if gui.State.Keys[lineNumber].Key == nil {
|
||||
return nil
|
||||
}
|
||||
if len(gui.State.Keys) > lineNumber {
|
||||
err := gui.handleMenuClose(g, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.State.Keys[lineNumber].Handler(g, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
// doing nothing for now
|
||||
// but it is needed for switch in newLineFocused
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderMenuOptions(g *gocui.Gui) error {
|
||||
optionsMap := map[string]string{
|
||||
"esc/q": gui.Tr.SLocalize("close"),
|
||||
"↑ ↓": gui.Tr.SLocalize("navigate"),
|
||||
"space": gui.Tr.SLocalize("execute"),
|
||||
}
|
||||
return gui.renderOptionsMap(g, optionsMap)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
// better to delete because for example after closing update confirmation panel,
|
||||
// the focus isn't set back to any of panels and one is unable to even quit
|
||||
//_, err := g.SetViewOnBottom(v.Name())
|
||||
err := g.DeleteView("menu")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.returnFocus(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) GetKey(binding Binding) string {
|
||||
r, ok := binding.Key.(rune)
|
||||
key := ""
|
||||
|
||||
if ok {
|
||||
key = string(r)
|
||||
} else if binding.KeyReadable != "" {
|
||||
key = binding.KeyReadable
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func (gui *Gui) GetMaxKeyLength(bindings []Binding) int {
|
||||
max := 0
|
||||
for _, binding := range bindings {
|
||||
keyLength := len(gui.GetKey(binding))
|
||||
if keyLength > max {
|
||||
max = keyLength
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
var (
|
||||
contentGlobal, contentPanel []string
|
||||
bindingsGlobal, bindingsPanel []Binding
|
||||
)
|
||||
// clear keys slice, so we don't have ghost elements
|
||||
gui.State.Keys = gui.State.Keys[:0]
|
||||
bindings := gui.GetKeybindings()
|
||||
padWidth := gui.GetMaxKeyLength(bindings)
|
||||
|
||||
for _, binding := range bindings {
|
||||
key := gui.GetKey(binding)
|
||||
if key != "" && binding.Description != "" {
|
||||
content := fmt.Sprintf("%s %s", utils.WithPadding(key, padWidth), binding.Description)
|
||||
switch binding.ViewName {
|
||||
case "":
|
||||
contentGlobal = append(contentGlobal, content)
|
||||
bindingsGlobal = append(bindingsGlobal, binding)
|
||||
case v.Name():
|
||||
contentPanel = append(contentPanel, content)
|
||||
bindingsPanel = append(bindingsPanel, binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append dummy element to have a separator between
|
||||
// panel and global keybindings
|
||||
contentPanel = append(contentPanel, "")
|
||||
bindingsPanel = append(bindingsPanel, Binding{})
|
||||
|
||||
content := append(contentPanel, contentGlobal...)
|
||||
gui.State.Keys = append(bindingsPanel, bindingsGlobal...)
|
||||
// append newline at the end so the last line would be selectable
|
||||
contentJoined := strings.Join(content, "\n") + "\n"
|
||||
|
||||
// y1-1 so there will not be an extra space at the end of panel
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, contentJoined)
|
||||
menuView, _ := g.SetView("menu", x0, y0, x1, y1-1, 0)
|
||||
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
|
||||
menuView.FgColor = gocui.ColorWhite
|
||||
|
||||
if err := gui.renderMenuOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(menuView, contentJoined)
|
||||
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
_, err := g.SetViewOnTop("menu")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchFocus(g, v, menuView)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
263
pkg/gui/merge_panel.go
Normal file
263
pkg/gui/merge_panel.go
Normal file
@@ -0,0 +1,263 @@
|
||||
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
|
||||
conflicts := make([]commands.Conflict, 0)
|
||||
var newConflict commands.Conflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
||||
newConflict = commands.Conflict{Start: i}
|
||||
} else if line == "=======" {
|
||||
newConflict.Middle = i
|
||||
} else if strings.HasPrefix(line, ">>>>>>> ") {
|
||||
newConflict.End = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
return conflicts, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
||||
}
|
||||
|
||||
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
||||
if len(conflicts) == 0 {
|
||||
return content, nil
|
||||
}
|
||||
conflict, remainingConflicts := gui.shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := color.FgWhite
|
||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String(), nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ConflictTop = true
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.ConflictTop = false
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
gui.State.ConflictIndex++
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
gui.State.ConflictIndex--
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
|
||||
return i == conflict.Middle ||
|
||||
i == conflict.Start ||
|
||||
i == conflict.End ||
|
||||
pick != "both" &&
|
||||
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
|
||||
(pick == "top" && i > conflict.Middle && i < conflict.End)
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Open(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
output := ""
|
||||
for i := 0; true; i++ {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !gui.isIndexToDelete(i, conflict, pick) {
|
||||
output += line
|
||||
}
|
||||
}
|
||||
gui.Log.Info(output)
|
||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.EditHistory.Push(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.EditHistory.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
prevContent := gui.State.EditHistory.Pop().(string)
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||
gui.pushFileSnapshot(g)
|
||||
pick := "bottom"
|
||||
if gui.State.ConflictTop {
|
||||
pick = "top"
|
||||
}
|
||||
err := gui.resolveConflict(g, conflict, pick)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.refreshMergePanel(g)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||
gui.pushFileSnapshot(g)
|
||||
err := gui.resolveConflict(g, conflict, "both")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
|
||||
cat, err := gui.catSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cat == "" {
|
||||
return nil
|
||||
}
|
||||
gui.State.Conflicts, err = gui.findConflicts(cat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.State.Conflicts) == 0 {
|
||||
return gui.handleCompleteMerge(g)
|
||||
} else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
|
||||
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
|
||||
}
|
||||
hasFocus := gui.currentViewName(g) == "main"
|
||||
if hasFocus {
|
||||
gui.renderMergeOptions(g)
|
||||
}
|
||||
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.scrollToConflict(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", content)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
|
||||
mainView, err := g.View("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(gui.State.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
|
||||
ox, _ := mainView.Origin()
|
||||
_, height := mainView.Size()
|
||||
conflictMiddle := (conflict.End + conflict.Start) / 2
|
||||
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
||||
return mainView.SetOrigin(ox, newOriginY)
|
||||
}
|
||||
|
||||
func (gui *Gui) switchToMerging(g *gocui.Gui) error {
|
||||
gui.State.ConflictIndex = 0
|
||||
gui.State.ConflictTop = true
|
||||
_, err := g.SetCurrentView("main")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
|
||||
return gui.renderOptionsMap(g, map[string]string{
|
||||
"↑ ↓": gui.Tr.SLocalize("selectHunk"),
|
||||
"← →": gui.Tr.SLocalize("navigateConflicts"),
|
||||
"space": gui.Tr.SLocalize("pickHunk"),
|
||||
"b": gui.Tr.SLocalize("pickBothHunks"),
|
||||
"z": gui.Tr.SLocalize("undo"),
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.refreshFiles(g)
|
||||
return gui.switchFocus(g, v, filesView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.stageSelectedFile(g)
|
||||
gui.refreshFiles(g)
|
||||
return gui.switchFocus(g, nil, filesView)
|
||||
}
|
||||
101
pkg/gui/stash_panel.go
Normal file
101
pkg/gui/stash_panel.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
v, err := g.View("stash")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
|
||||
v.Clear()
|
||||
for _, stashEntry := range gui.State.StashEntries {
|
||||
fmt.Fprintln(v, stashEntry.DisplayString)
|
||||
}
|
||||
return gui.resetOrigin(v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||
if len(gui.State.StashEntries) == 0 {
|
||||
return nil
|
||||
}
|
||||
stashView, _ := gui.g.View("stash")
|
||||
lineNumber := gui.getItemPosition(stashView)
|
||||
return &gui.State.StashEntries[lineNumber]
|
||||
}
|
||||
|
||||
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
|
||||
return gui.renderGlobalOptions(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.renderStashOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
return
|
||||
}
|
||||
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
|
||||
gui.renderString(g, "main", diff)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "apply")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "pop")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
title := gui.Tr.SLocalize("StashDrop")
|
||||
message := gui.Tr.SLocalize("SureDropStashEntry")
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "drop")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
errorMessage := gui.Tr.TemplateLocalize(
|
||||
"NoStashTo",
|
||||
Teml{
|
||||
"method": method,
|
||||
},
|
||||
)
|
||||
return gui.createErrorPanel(g, errorMessage)
|
||||
}
|
||||
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.refreshStashEntries(g)
|
||||
return gui.refreshFiles(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
}
|
||||
gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.refreshStashEntries(g)
|
||||
return gui.refreshFiles(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
88
pkg/gui/status_panel.go
Normal file
88
pkg/gui/status_panel.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||
v, err := g.View("status")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// for some reason if this isn't wrapped in an update the clear seems to
|
||||
// be applied after the other things or something like that; the panel's
|
||||
// contents end up cleared
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v.Clear()
|
||||
pushables, pullables := gui.GitCommand.UpstreamDifferenceCount()
|
||||
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
||||
branches := gui.State.Branches
|
||||
if err := gui.updateHasMergeConflictStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
if gui.State.HasMergeConflicts {
|
||||
fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow))
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
branch := branches[0]
|
||||
name := utils.ColoredString(branch.Name, branch.GetColor())
|
||||
repo := utils.GetCurrentRepoName()
|
||||
fmt.Fprint(v, " "+repo+" → "+name)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
|
||||
return gui.renderGlobalOptions(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
|
||||
return gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("CheckingForUpdates"))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
dashboardString := fmt.Sprintf(
|
||||
"%s\n\n%s\n\n%s\n\n%s\n\n%s",
|
||||
lazygitTitle(),
|
||||
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
|
||||
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
"Tutorial: https://www.youtube.com/watch?v=VDXvbHZYeKY",
|
||||
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
|
||||
)
|
||||
|
||||
if err := gui.renderString(g, "main", dashboardString); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderStatusOptions(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.openFile(gui.Config.GetUserConfig().ConfigFileUsed())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
filename := gui.Config.GetUserConfig().ConfigFileUsed()
|
||||
return gui.editFile(filename)
|
||||
}
|
||||
|
||||
func lazygitTitle() string {
|
||||
return `
|
||||
_ _ _
|
||||
| | (_) |
|
||||
| | __ _ _____ _ __ _ _| |_
|
||||
| |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __|
|
||||
| | (_| |/ /| |_| | (_| | | |_
|
||||
|_|\__,_/___|\__, |\__, |_|\__|
|
||||
__/ | __/ |
|
||||
|___/ |___/ `
|
||||
}
|
||||
54
pkg/gui/theme.go
Normal file
54
pkg/gui/theme.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// GetAttribute gets the gocui color attribute from the string
|
||||
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
|
||||
colorMap := map[string]gocui.Attribute{
|
||||
"default": gocui.ColorDefault,
|
||||
"black": gocui.ColorBlack,
|
||||
"red": gocui.ColorRed,
|
||||
"green": gocui.ColorGreen,
|
||||
"yellow": gocui.ColorYellow,
|
||||
"blue": gocui.ColorBlue,
|
||||
"magenta": gocui.ColorMagenta,
|
||||
"cyan": gocui.ColorCyan,
|
||||
"white": gocui.ColorWhite,
|
||||
"bold": gocui.AttrBold,
|
||||
"reverse": gocui.AttrReverse,
|
||||
"underline": gocui.AttrUnderline,
|
||||
}
|
||||
value, present := colorMap[key]
|
||||
if present {
|
||||
return value
|
||||
}
|
||||
return gocui.ColorWhite
|
||||
}
|
||||
|
||||
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
||||
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
|
||||
var attribute gocui.Attribute
|
||||
for _, key := range keys {
|
||||
attribute = attribute | gui.GetAttribute(key)
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
// GetOptionsPanelTextColor gets the color of the options panel text
|
||||
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
|
||||
return gui.GetColor(optionsColor), nil
|
||||
}
|
||||
|
||||
// SetColorScheme sets the color scheme for the app based on the user config
|
||||
func (gui *Gui) SetColorScheme() error {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor")
|
||||
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
|
||||
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
|
||||
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
|
||||
return nil
|
||||
}
|
||||
65
pkg/gui/updates.go
Normal file
65
pkg/gui/updates.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func (gui *Gui) showUpdatePrompt(newVersion string) error {
|
||||
title := "New version available!"
|
||||
message := "Download latest version? (enter/esc)"
|
||||
currentView := gui.g.CurrentView()
|
||||
return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.startUpdating(newVersion)
|
||||
return nil
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
if newVersion == "" {
|
||||
return gui.createErrorPanel(gui.g, "New version not found")
|
||||
}
|
||||
return gui.showUpdatePrompt(newVersion)
|
||||
}
|
||||
|
||||
func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error {
|
||||
if err != nil {
|
||||
// ignoring the error for now so that I'm not annoying users
|
||||
gui.Log.Error(err.Error())
|
||||
return nil
|
||||
}
|
||||
if newVersion == "" {
|
||||
return nil
|
||||
}
|
||||
if gui.Config.GetUserConfig().Get("update.method") == "background" {
|
||||
gui.startUpdating(newVersion)
|
||||
return nil
|
||||
}
|
||||
return gui.showUpdatePrompt(newVersion)
|
||||
}
|
||||
|
||||
func (gui *Gui) startUpdating(newVersion string) {
|
||||
gui.State.Updating = true
|
||||
gui.statusManager.addWaitingStatus("updating")
|
||||
gui.Updater.Update(newVersion, gui.onUpdateFinish)
|
||||
}
|
||||
|
||||
func (gui *Gui) onUpdateFinish(err error) error {
|
||||
gui.State.Updating = false
|
||||
gui.statusManager.removeStatus("updating")
|
||||
if err := gui.renderString(gui.g, "appStatus", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, "Update failed: "+err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
|
||||
title := "Currently Updating"
|
||||
message := "An update is in progress. Are you sure you want to quit?"
|
||||
return gui.createConfirmationPanel(gui.g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
}
|
||||
301
pkg/gui/view_helpers.go
Normal file
301
pkg/gui/view_helpers.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spkg/bom"
|
||||
)
|
||||
|
||||
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
|
||||
|
||||
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||
gui.refreshBranches(g)
|
||||
gui.refreshFiles(g)
|
||||
gui.refreshCommits(g)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
||||
var focusedViewName string
|
||||
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
||||
focusedViewName = cyclableViews[0]
|
||||
} else {
|
||||
for i := range cyclableViews {
|
||||
if v.Name() == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i+1]
|
||||
break
|
||||
}
|
||||
if i == len(cyclableViews)-1 {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": v.Name(),
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
focusedView, err := g.View(focusedViewName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
}
|
||||
|
||||
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||
var focusedViewName string
|
||||
if v == nil || v.Name() == cyclableViews[0] {
|
||||
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
||||
} else {
|
||||
for i := range cyclableViews {
|
||||
if v.Name() == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
|
||||
break
|
||||
}
|
||||
if i == len(cyclableViews)-1 {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": v.Name(),
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
focusedView, err := g.View(focusedViewName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.switchFocus(g, v, focusedView)
|
||||
}
|
||||
|
||||
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
mainView.SetOrigin(0, 0)
|
||||
|
||||
switch v.Name() {
|
||||
case "menu":
|
||||
return gui.handleMenuSelect(g, v)
|
||||
case "status":
|
||||
return gui.handleStatusSelect(g, v)
|
||||
case "files":
|
||||
return gui.handleFileSelect(g, v)
|
||||
case "branches":
|
||||
return gui.handleBranchSelect(g, v)
|
||||
case "confirmation":
|
||||
return nil
|
||||
case "commitMessage":
|
||||
return gui.handleCommitFocused(g, v)
|
||||
case "main":
|
||||
// TODO: pull this out into a 'view focused' function
|
||||
gui.refreshMergePanel(g)
|
||||
v.Highlight = false
|
||||
return nil
|
||||
case "commits":
|
||||
return gui.handleCommitSelect(g, v)
|
||||
case "stash":
|
||||
return gui.handleStashEntrySelect(g, v)
|
||||
default:
|
||||
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||
previousView, err := g.View(gui.State.PreviousView)
|
||||
if err != nil {
|
||||
// always fall back to files view if there's no 'previous' view stored
|
||||
previousView, err = g.View("files")
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
return gui.switchFocus(g, v, previousView)
|
||||
}
|
||||
|
||||
// pass in oldView = nil if you don't want to be able to return to your old view
|
||||
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||
// we assume we'll never want to return focus to a confirmation panel i.e.
|
||||
// we should never stack confirmation panels
|
||||
if oldView != nil && oldView.Name() != "confirmation" {
|
||||
oldView.Highlight = false
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"settingPreviewsViewTo",
|
||||
Teml{
|
||||
"oldViewName": oldView.Name(),
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
gui.State.PreviousView = oldView.Name()
|
||||
}
|
||||
newView.Highlight = true
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"newFocusedViewIs",
|
||||
Teml{
|
||||
"newFocusedView": newView.Name(),
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
if _, err := g.SetCurrentView(newView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
g.Cursor = newView.Editable
|
||||
|
||||
return gui.newLineFocused(g, newView)
|
||||
}
|
||||
|
||||
func (gui *Gui) getItemPosition(v *gocui.View) int {
|
||||
gui.correctCursor(v)
|
||||
_, cy := v.Cursor()
|
||||
_, oy := v.Origin()
|
||||
return oy + cy
|
||||
}
|
||||
|
||||
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||
// swallowing cursor movements in main
|
||||
// TODO: pull this out
|
||||
if v == nil || v.Name() == "main" {
|
||||
return nil
|
||||
}
|
||||
|
||||
ox, oy := v.Origin()
|
||||
cx, cy := v.Cursor()
|
||||
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
|
||||
if err := v.SetOrigin(ox, oy-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.newLineFocused(g, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||
// swallowing cursor movements in main
|
||||
// TODO: pull this out
|
||||
if v == nil || v.Name() == "main" {
|
||||
return nil
|
||||
}
|
||||
cx, cy := v.Cursor()
|
||||
ox, oy := v.Origin()
|
||||
if cy+oy >= len(v.BufferLines())-2 {
|
||||
return nil
|
||||
}
|
||||
if err := v.SetCursor(cx, cy+1); err != nil {
|
||||
if err := v.SetOrigin(ox, oy+1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.newLineFocused(g, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) resetOrigin(v *gocui.View) error {
|
||||
if err := v.SetCursor(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.SetOrigin(0, 0)
|
||||
}
|
||||
|
||||
// if the cursor down past the last item, move it to the last line
|
||||
func (gui *Gui) correctCursor(v *gocui.View) error {
|
||||
cx, cy := v.Cursor()
|
||||
_, oy := v.Origin()
|
||||
lineCount := len(v.BufferLines()) - 2
|
||||
if cy >= lineCount-oy {
|
||||
return v.SetCursor(cx, lineCount-oy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v, err := g.View(viewName)
|
||||
// just in case the view disappeared as this function was called, we'll
|
||||
// silently return if it's not found
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
v.Clear()
|
||||
output := string(bom.Clean([]byte(s)))
|
||||
output = utils.NormalizeLinefeeds(output)
|
||||
fmt.Fprint(v, output)
|
||||
v.Wrap = true
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
|
||||
optionsArray := make([]string, 0)
|
||||
for key, description := range optionsMap {
|
||||
optionsArray = append(optionsArray, key+": "+description)
|
||||
}
|
||||
sort.Strings(optionsArray)
|
||||
return strings.Join(optionsArray, ", ")
|
||||
}
|
||||
|
||||
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
|
||||
return gui.renderString(g, "options", gui.optionsMapToString(optionsMap))
|
||||
}
|
||||
|
||||
// TODO: refactor properly
|
||||
// i'm so sorry but had to add this getBranchesView
|
||||
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("files")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("commits")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("commitMessage")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("branches")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
||||
return strings.TrimSpace(v.Buffer())
|
||||
}
|
||||
|
||||
func (gui *Gui) currentViewName(g *gocui.Gui) string {
|
||||
currentView := g.CurrentView()
|
||||
return currentView.Name()
|
||||
}
|
||||
|
||||
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
|
||||
v := g.CurrentView()
|
||||
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
|
||||
return gui.resizePopupPanel(g, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
// If the confirmation panel is already displayed, just resize the width,
|
||||
// otherwise continue
|
||||
content := v.Buffer()
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
|
||||
vx0, vy0, vx1, vy1 := v.Dimensions()
|
||||
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
||||
return nil
|
||||
}
|
||||
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
|
||||
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||
return err
|
||||
}
|
||||
375
pkg/i18n/dutch.go
Normal file
375
pkg/i18n/dutch.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// addDutch will add all dutch translations
|
||||
func addDutch(i18nObject *i18n.Bundle) error {
|
||||
|
||||
// add the translations
|
||||
return i18nObject.AddMessages(language.Dutch,
|
||||
&i18n.Message{
|
||||
ID: "NotEnoughSpace",
|
||||
Other: "Niet genoeg ruimte om de panelen te renderen",
|
||||
}, &i18n.Message{
|
||||
ID: "DiffTitle",
|
||||
Other: "Diff",
|
||||
}, &i18n.Message{
|
||||
ID: "FilesTitle",
|
||||
Other: "Bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchesTitle",
|
||||
Other: "Branches",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commits",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Stash",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitMessage",
|
||||
Other: "Commit Bericht",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "Commit Veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChangesWithEditor",
|
||||
Other: "commit Veranderingen met de git editor",
|
||||
}, &i18n.Message{
|
||||
ID: "StatusTitle",
|
||||
Other: "Status",
|
||||
}, &i18n.Message{
|
||||
ID: "GlobalTitle",
|
||||
Other: "Global",
|
||||
}, &i18n.Message{
|
||||
ID: "navigate",
|
||||
Other: "navigeer",
|
||||
}, &i18n.Message{
|
||||
ID: "menu",
|
||||
Other: "menu",
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "uitvoeren",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "stash-bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "open",
|
||||
}, &i18n.Message{
|
||||
ID: "ignore",
|
||||
Other: "negeren",
|
||||
}, &i18n.Message{
|
||||
ID: "delete",
|
||||
Other: "verwijderen",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStaged",
|
||||
Other: "toggle staged",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStagedAll",
|
||||
Other: "toggle staged alle",
|
||||
}, &i18n.Message{
|
||||
ID: "refresh",
|
||||
Other: "verversen",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "verandering toevoegen",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "verander",
|
||||
}, &i18n.Message{
|
||||
ID: "scroll",
|
||||
Other: "scroll",
|
||||
}, &i18n.Message{
|
||||
ID: "abortMerge",
|
||||
Other: "samenvoegen afbreken",
|
||||
}, &i18n.Message{
|
||||
ID: "resolveMergeConflicts",
|
||||
Other: "verhelp samenvoegen fouten",
|
||||
}, &i18n.Message{
|
||||
ID: "checkout",
|
||||
Other: "uitchecken",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "Geen Bestanden verandert",
|
||||
}, &i18n.Message{
|
||||
ID: "FileHasNoUnstagedChanges",
|
||||
Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotGitAdd",
|
||||
Other: "Kan commando niet uitvoeren git add --path untracked files",
|
||||
}, &i18n.Message{
|
||||
ID: "CantIgnoreTrackFiles",
|
||||
Other: "Kan gevolgde bestanden niet negeren",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStagedFilesToCommit",
|
||||
Other: "Er zijn geen staged bestanden om te commiten",
|
||||
}, &i18n.Message{
|
||||
ID: "NoFilesDisplay",
|
||||
Other: "Geen bestanden om te laten zien",
|
||||
}, &i18n.Message{
|
||||
ID: "PullWait",
|
||||
Other: "Pulling...",
|
||||
}, &i18n.Message{
|
||||
ID: "PushWait",
|
||||
Other: "Pushing...",
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "Dit bestand heeft geen merge conflicten",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? het kan dat je hierdoor bestanden verliest",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijdert)",
|
||||
}, &i18n.Message{
|
||||
ID: "AlreadyCheckedOutBranch",
|
||||
Other: "Je hebt uitgecheckt op deze branch",
|
||||
}, &i18n.Message{
|
||||
ID: "SureForceCheckout",
|
||||
Other: "Weet je zeker dat je het uitchecken wil forceren? al je locale verandering zullen worden verwijdert",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceCheckoutBranch",
|
||||
Other: "Forceer uitchecken op deze branch",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchName",
|
||||
Other: "Branch naam",
|
||||
}, &i18n.Message{
|
||||
ID: "NewBranchNameBranchOff",
|
||||
Other: "Nieuw branch naam (Branch is afgeleid van {{.branchName}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CantDeleteCheckOutBranch",
|
||||
Other: "Je kan een uitgecheckte branch niet verwijderen!",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranch",
|
||||
Other: "Verwijder branch",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranchMessage",
|
||||
Other: "Weet je zeker dat je branch {{.selectedBranchName}} wil verwijderen?",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceDeleteBranchMessage",
|
||||
Other: "Weet je zeker dat je branch {{.selectedBranchName}} geforceerd wil verwijderen?",
|
||||
}, &i18n.Message{
|
||||
ID: "CantMergeBranchIntoItself",
|
||||
Other: "Je kan niet een branch in zichzelf mergen",
|
||||
}, &i18n.Message{
|
||||
ID: "forceCheckout",
|
||||
Other: "forceer checkout",
|
||||
}, &i18n.Message{
|
||||
ID: "merge",
|
||||
Other: "merge",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutByName",
|
||||
Other: "uitchecken bij naam",
|
||||
}, &i18n.Message{
|
||||
ID: "newBranch",
|
||||
Other: "nieuwe branch",
|
||||
}, &i18n.Message{
|
||||
ID: "deleteBranch",
|
||||
Other: "verwijder branch",
|
||||
}, &i18n.Message{
|
||||
ID: "forceDeleteBranch",
|
||||
Other: "verwijder branch (forceer)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchesThisRepo",
|
||||
Other: "Geen branches voor deze repo",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackingThisBranch",
|
||||
Other: "deze branch wordt niet gevolgd",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitWithoutMessageErr",
|
||||
Other: "Je kan geen commit maken zonder commit bericht",
|
||||
}, &i18n.Message{
|
||||
ID: "CloseConfirm",
|
||||
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
|
||||
}, &i18n.Message{
|
||||
ID: "close",
|
||||
Other: "sluiten",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetThisCommit",
|
||||
Other: "Weet je het zeker dat je wil resetten naar deze commit?",
|
||||
}, &i18n.Message{
|
||||
ID: "ResetToCommit",
|
||||
Other: "Reset Naar Commit",
|
||||
}, &i18n.Message{
|
||||
ID: "squashDown",
|
||||
Other: "squash beneden",
|
||||
}, &i18n.Message{
|
||||
ID: "rename",
|
||||
Other: "hernoem",
|
||||
}, &i18n.Message{
|
||||
ID: "resetToThisCommit",
|
||||
Other: "reset naar deze commit",
|
||||
}, &i18n.Message{
|
||||
ID: "fixupCommit",
|
||||
Other: "Fixup commit",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "Er zijn geen commits voor deze branch",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlySquashTopmostCommit",
|
||||
Other: "Kan alleen bovenste commit squashen",
|
||||
}, &i18n.Message{
|
||||
ID: "YouNoCommitsToSquash",
|
||||
Other: "Je hebt geen commits om mee te squashen",
|
||||
}, &i18n.Message{
|
||||
ID: "CantFixupWhileUnstagedChanges",
|
||||
Other: "Kan geen Fixup uitvoeren op unstaged veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "Fixup",
|
||||
Other: "Fixup",
|
||||
}, &i18n.Message{
|
||||
ID: "SureFixupThisCommit",
|
||||
Other: "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlyRenameTopCommit",
|
||||
Other: "Je kan alleen de bovenste commit hernoemen",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommit",
|
||||
Other: "hernoem commit",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommitEditor",
|
||||
Other: "rename commit with editor",
|
||||
}, &i18n.Message{
|
||||
ID: "PotentialErrInGetselectedCommit",
|
||||
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "Geen commits voor deze branch",
|
||||
}, &i18n.Message{
|
||||
ID: "Error",
|
||||
Other: "Fout",
|
||||
}, &i18n.Message{
|
||||
ID: "resizingPopupPanel",
|
||||
Other: "resizen popup paneel",
|
||||
}, &i18n.Message{
|
||||
ID: "RunningSubprocess",
|
||||
Other: "subprocess lopend",
|
||||
}, &i18n.Message{
|
||||
ID: "selectHunk",
|
||||
Other: "selecteer Hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "navigateConflicts",
|
||||
Other: "navigeer conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "pickHunk",
|
||||
Other: "kies Hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "pickBothHunks",
|
||||
Other: "kies bijde hunks",
|
||||
}, &i18n.Message{
|
||||
ID: "undo",
|
||||
Other: "ongedaan maken",
|
||||
}, &i18n.Message{
|
||||
ID: "pop",
|
||||
Other: "pop",
|
||||
}, &i18n.Message{
|
||||
ID: "drop",
|
||||
Other: "drop",
|
||||
}, &i18n.Message{
|
||||
ID: "apply",
|
||||
Other: "toepassen",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashEntries",
|
||||
Other: "Geen stash items",
|
||||
}, &i18n.Message{
|
||||
ID: "StashDrop",
|
||||
Other: "Stash drop",
|
||||
}, &i18n.Message{
|
||||
ID: "SureDropStashEntry",
|
||||
Other: "Weet je het zeker dat je deze stash entry wil laten vallen?",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashTo",
|
||||
Other: "Geen stash voor {{.method}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackedStagedFilesStash",
|
||||
Other: "Je hebt geen tracked/staged bestanden om te laten stashen",
|
||||
}, &i18n.Message{
|
||||
ID: "StashChanges",
|
||||
Other: "Stash veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "IssntListOfViews",
|
||||
Other: "{{.name}} is niet in de lijst van weergaves",
|
||||
}, &i18n.Message{
|
||||
ID: "NoViewMachingNewLineFocusedSwitchStatement",
|
||||
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
|
||||
}, &i18n.Message{
|
||||
ID: "settingPreviewsViewTo",
|
||||
Other: "vorige weergave instellen op: {{.oldViewName}}",
|
||||
}, &i18n.Message{
|
||||
ID: "newFocusedViewIs",
|
||||
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
|
||||
}, &i18n.Message{
|
||||
ID: "CantCloseConfirmationPrompt",
|
||||
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "Geen veranderde files",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "maak bestandsvenster leeg",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Merge afgebroken",
|
||||
}, &i18n.Message{
|
||||
ID: "OpenConfig",
|
||||
Other: "open config file",
|
||||
}, &i18n.Message{
|
||||
ID: "EditConfig",
|
||||
Other: "verander config file",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePush",
|
||||
Other: "Forceer push",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePushPrompt",
|
||||
Other: "Jou branch is afgeweken van de remote branch. Druk 'esc' om te anuleren, of 'enter' om geforceert te pushen.",
|
||||
}, &i18n.Message{
|
||||
ID: "checkForUpdate",
|
||||
Other: "check voor updates",
|
||||
}, &i18n.Message{
|
||||
ID: "CheckingForUpdates",
|
||||
Other: "checken voor updates...",
|
||||
}, &i18n.Message{
|
||||
ID: "OnLatestVersionErr",
|
||||
Other: "Je hebt al de laatste versie",
|
||||
}, &i18n.Message{
|
||||
ID: "MajorVersionErr",
|
||||
Other: "Nieuwe versie ({{.newVersion}}) is niet teruggaand compatibele vergeleken met de huidige versie ({{.currentVersion}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CouldNotFindBinaryErr",
|
||||
Other: "Kon geen binary vinden op {{.url}}",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingTitle",
|
||||
Other: "Help maak lazygit beter",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `Verwijder als untracked / uitchecken wordt gevolgd (ga weg)`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `verander bestand`,
|
||||
}, &i18n.Message{
|
||||
ID: "openFile",
|
||||
Other: `open bestand`,
|
||||
}, &i18n.Message{
|
||||
ID: "ignoreFile",
|
||||
Other: `voeg toe aan .gitignore`,
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `refresh bestanden`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `harde reset`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `merge in met huidige checked out branch`,
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Weet je zeker dat je dit programma wil sluiten?`,
|
||||
},
|
||||
)
|
||||
}
|
||||
395
pkg/i18n/english.go
Normal file
395
pkg/i18n/english.go
Normal file
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
|
||||
Todo list when making a new translation
|
||||
- Copy this file and rename it to the language you want to translate to like someLanguage.go
|
||||
- Change the addEnglish() name to the language you want to translate to like addSomeLanguage()
|
||||
- change the first function argument of i18nObject.AddMessages( to the language you want to translate to like language.SomeLanguage
|
||||
- Remove this todo and the about section
|
||||
|
||||
*/
|
||||
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
|
||||
return i18nObject.AddMessages(language.English,
|
||||
&i18n.Message{
|
||||
ID: "NotEnoughSpace",
|
||||
Other: "Not enough space to render panels",
|
||||
}, &i18n.Message{
|
||||
ID: "DiffTitle",
|
||||
Other: "Diff",
|
||||
}, &i18n.Message{
|
||||
ID: "FilesTitle",
|
||||
Other: "Files",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchesTitle",
|
||||
Other: "Branches",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commits",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Stash",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitMessage",
|
||||
Other: "Commit message",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "commit changes",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChangesWithEditor",
|
||||
Other: "commit changes using git editor",
|
||||
}, &i18n.Message{
|
||||
ID: "StatusTitle",
|
||||
Other: "Status",
|
||||
}, &i18n.Message{
|
||||
ID: "GlobalTitle",
|
||||
Other: "Global",
|
||||
}, &i18n.Message{
|
||||
ID: "navigate",
|
||||
Other: "navigate",
|
||||
}, &i18n.Message{
|
||||
ID: "menu",
|
||||
Other: "menu",
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "execute",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "stash files",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "open",
|
||||
}, &i18n.Message{
|
||||
ID: "ignore",
|
||||
Other: "ignore",
|
||||
}, &i18n.Message{
|
||||
ID: "delete",
|
||||
Other: "delete",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStaged",
|
||||
Other: "toggle staged",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStagedAll",
|
||||
Other: "stage/unstage all",
|
||||
}, &i18n.Message{
|
||||
ID: "refresh",
|
||||
Other: "refresh",
|
||||
}, &i18n.Message{
|
||||
ID: "push",
|
||||
Other: "push",
|
||||
}, &i18n.Message{
|
||||
ID: "pull",
|
||||
Other: "pull",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "add patch",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "edit",
|
||||
}, &i18n.Message{
|
||||
ID: "scroll",
|
||||
Other: "scroll",
|
||||
}, &i18n.Message{
|
||||
ID: "abortMerge",
|
||||
Other: "abort merge",
|
||||
}, &i18n.Message{
|
||||
ID: "resolveMergeConflicts",
|
||||
Other: "resolve merge conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "checkout",
|
||||
Other: "checkout",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "No changed files",
|
||||
}, &i18n.Message{
|
||||
ID: "FileHasNoUnstagedChanges",
|
||||
Other: "File has no unstaged changes to add",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotGitAdd",
|
||||
Other: "Cannot git add --patch untracked files",
|
||||
}, &i18n.Message{
|
||||
ID: "CantIgnoreTrackFiles",
|
||||
Other: "Cannot ignore tracked files",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStagedFilesToCommit",
|
||||
Other: "There are no staged files to commit",
|
||||
}, &i18n.Message{
|
||||
ID: "NoFilesDisplay",
|
||||
Other: "No file to display",
|
||||
}, &i18n.Message{
|
||||
ID: "NotAFile",
|
||||
Other: "Not a file",
|
||||
}, &i18n.Message{
|
||||
ID: "PullWait",
|
||||
Other: "Pulling...",
|
||||
}, &i18n.Message{
|
||||
ID: "PushWait",
|
||||
Other: "Pushing...",
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "This file has no merge conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Are you sure you want `reset --hard HEAD`? You may lose changes",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
|
||||
}, &i18n.Message{
|
||||
ID: "AlreadyCheckedOutBranch",
|
||||
Other: "You have already checked out this branch",
|
||||
}, &i18n.Message{
|
||||
ID: "SureForceCheckout",
|
||||
Other: "Are you sure you want force checkout? You will lose all local changes",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceCheckoutBranch",
|
||||
Other: "Force Checkout Branch",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchName",
|
||||
Other: "Branch name",
|
||||
}, &i18n.Message{
|
||||
ID: "NewBranchNameBranchOff",
|
||||
Other: "New Branch Name (Branch is off of {{.branchName}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CantDeleteCheckOutBranch",
|
||||
Other: "You cannot delete the checked out branch!",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranch",
|
||||
Other: "Delete Branch",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranchMessage",
|
||||
Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceDeleteBranchMessage",
|
||||
Other: "Are you sure you want to force delete the branch {{.selectedBranchName}}?",
|
||||
}, &i18n.Message{
|
||||
ID: "CantMergeBranchIntoItself",
|
||||
Other: "You cannot merge a branch into itself",
|
||||
}, &i18n.Message{
|
||||
ID: "forceCheckout",
|
||||
Other: "force checkout",
|
||||
}, &i18n.Message{
|
||||
ID: "merge",
|
||||
Other: "merge",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutByName",
|
||||
Other: "checkout by name",
|
||||
}, &i18n.Message{
|
||||
ID: "newBranch",
|
||||
Other: "new branch",
|
||||
}, &i18n.Message{
|
||||
ID: "deleteBranch",
|
||||
Other: "delete branch",
|
||||
}, &i18n.Message{
|
||||
ID: "forceDeleteBranch",
|
||||
Other: "delete branch (force)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchesThisRepo",
|
||||
Other: "No branches for this repo",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackingThisBranch",
|
||||
Other: "There is no tracking for this branch",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitWithoutMessageErr",
|
||||
Other: "You cannot commit without a commit message",
|
||||
}, &i18n.Message{
|
||||
ID: "CloseConfirm",
|
||||
Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
|
||||
}, &i18n.Message{
|
||||
ID: "close",
|
||||
Other: "close",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetThisCommit",
|
||||
Other: "Are you sure you want to reset to this commit?",
|
||||
}, &i18n.Message{
|
||||
ID: "ResetToCommit",
|
||||
Other: "Reset To Commit",
|
||||
}, &i18n.Message{
|
||||
ID: "squashDown",
|
||||
Other: "squash down",
|
||||
}, &i18n.Message{
|
||||
ID: "rename",
|
||||
Other: "rename",
|
||||
}, &i18n.Message{
|
||||
ID: "resetToThisCommit",
|
||||
Other: "reset to this commit",
|
||||
}, &i18n.Message{
|
||||
ID: "fixupCommit",
|
||||
Other: "fixup commit",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "No commits for this branch",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlySquashTopmostCommit",
|
||||
Other: "Can only squash topmost commit",
|
||||
}, &i18n.Message{
|
||||
ID: "YouNoCommitsToSquash",
|
||||
Other: "You have no commits to squash with",
|
||||
}, &i18n.Message{
|
||||
ID: "CantFixupWhileUnstagedChanges",
|
||||
Other: "Can't fixup while there are unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "Fixup",
|
||||
Other: "Fixup",
|
||||
}, &i18n.Message{
|
||||
ID: "SureFixupThisCommit",
|
||||
Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlyRenameTopCommit",
|
||||
Other: "Can only rename topmost commit",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommit",
|
||||
Other: "rename commit",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommitEditor",
|
||||
Other: "rename commit with editor",
|
||||
}, &i18n.Message{
|
||||
ID: "PotentialErrInGetselectedCommit",
|
||||
Other: "potential error in getSelected Commit (mismatched ui and state)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "No commits for this branch",
|
||||
}, &i18n.Message{
|
||||
ID: "Error",
|
||||
Other: "Error",
|
||||
}, &i18n.Message{
|
||||
ID: "resizingPopupPanel",
|
||||
Other: "resizing popup panel",
|
||||
}, &i18n.Message{
|
||||
ID: "RunningSubprocess",
|
||||
Other: "running subprocess",
|
||||
}, &i18n.Message{
|
||||
ID: "selectHunk",
|
||||
Other: "select hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "navigateConflicts",
|
||||
Other: "navigate conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "pickHunk",
|
||||
Other: "pick hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "pickBothHunks",
|
||||
Other: "pick both hunks",
|
||||
}, &i18n.Message{
|
||||
ID: "undo",
|
||||
Other: "undo",
|
||||
}, &i18n.Message{
|
||||
ID: "pop",
|
||||
Other: "pop",
|
||||
}, &i18n.Message{
|
||||
ID: "drop",
|
||||
Other: "drop",
|
||||
}, &i18n.Message{
|
||||
ID: "apply",
|
||||
Other: "apply",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashEntries",
|
||||
Other: "No stash entries",
|
||||
}, &i18n.Message{
|
||||
ID: "StashDrop",
|
||||
Other: "Stash drop",
|
||||
}, &i18n.Message{
|
||||
ID: "SureDropStashEntry",
|
||||
Other: "Are you sure you want to drop this stash entry?",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashTo",
|
||||
Other: "No stash to {{.method}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackedStagedFilesStash",
|
||||
Other: "You have no tracked/staged files to stash",
|
||||
}, &i18n.Message{
|
||||
ID: "StashChanges",
|
||||
Other: "Stash changes",
|
||||
}, &i18n.Message{
|
||||
ID: "IssntListOfViews",
|
||||
Other: "{{.name}} is not in the list of views",
|
||||
}, &i18n.Message{
|
||||
ID: "NoViewMachingNewLineFocusedSwitchStatement",
|
||||
Other: "No view matching newLineFocused switch statement",
|
||||
}, &i18n.Message{
|
||||
ID: "settingPreviewsViewTo",
|
||||
Other: "setting previous view to: {{.oldViewName}}",
|
||||
}, &i18n.Message{
|
||||
ID: "newFocusedViewIs",
|
||||
Other: "new focused view is {{.newFocusedView}}",
|
||||
}, &i18n.Message{
|
||||
ID: "CantCloseConfirmationPrompt",
|
||||
Other: "Could not close confirmation prompt: {{.error}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "No changed files",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "Clear file panel",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Merge aborted",
|
||||
}, &i18n.Message{
|
||||
ID: "OpenConfig",
|
||||
Other: "open config file",
|
||||
}, &i18n.Message{
|
||||
ID: "EditConfig",
|
||||
Other: "edit config file",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePush",
|
||||
Other: "Force push",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePushPrompt",
|
||||
Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.",
|
||||
}, &i18n.Message{
|
||||
ID: "checkForUpdate",
|
||||
Other: "check for update",
|
||||
}, &i18n.Message{
|
||||
ID: "CheckingForUpdates",
|
||||
Other: "Checking for updates...",
|
||||
}, &i18n.Message{
|
||||
ID: "OnLatestVersionErr",
|
||||
Other: "You already have the latest version",
|
||||
}, &i18n.Message{
|
||||
ID: "MajorVersionErr",
|
||||
Other: "New version ({{.newVersion}}) has non-backwards compatible changes compared to the current version ({{.currentVersion}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CouldNotFindBinaryErr",
|
||||
Other: "Could not find any binary at {{.url}}",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingTitle",
|
||||
Other: "Help make lazygit better",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "GitconfigParseErr",
|
||||
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `delete if untracked / checkout if tracked`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `edit file`,
|
||||
}, &i18n.Message{
|
||||
ID: "openFile",
|
||||
Other: `open file`,
|
||||
}, &i18n.Message{
|
||||
ID: "ignoreFile",
|
||||
Other: `add to .gitignore`,
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `refresh files`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `reset hard`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `merge into currently checked out branch`,
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Are you sure you want to quit?`,
|
||||
},
|
||||
)
|
||||
}
|
||||
102
pkg/i18n/i18n.go
Normal file
102
pkg/i18n/i18n.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"github.com/cloudfoundry/jibber_jabber"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Teml is short for template used to make the required map[string]interface{} shorter when using gui.Tr.SLocalize and gui.Tr.TemplateLocalize
|
||||
type Teml map[string]interface{}
|
||||
|
||||
// Localizer will translate a message into the user's language
|
||||
type Localizer struct {
|
||||
i18nLocalizer *i18n.Localizer
|
||||
language string
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
// NewLocalizer creates a new Localizer
|
||||
func NewLocalizer(log *logrus.Entry) *Localizer {
|
||||
userLang := detectLanguage(jibber_jabber.DetectLanguage)
|
||||
|
||||
log.Info("language: " + userLang)
|
||||
|
||||
return setupLocalizer(log, userLang)
|
||||
}
|
||||
|
||||
// Localize handels the translations
|
||||
// expects i18n.LocalizeConfig as input: https://godoc.org/github.com/nicksnyder/go-i18n/v2/i18n#Localizer.MustLocalize
|
||||
// output: translated string
|
||||
func (l *Localizer) Localize(config *i18n.LocalizeConfig) string {
|
||||
return l.i18nLocalizer.MustLocalize(config)
|
||||
}
|
||||
|
||||
// SLocalize (short localize) is for 1 line localizations
|
||||
// ID: The id that is used in the .toml translation files
|
||||
// Other: the default message it needs to return if there is no translation found or the system is english
|
||||
func (l *Localizer) SLocalize(ID string) string {
|
||||
return l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: ID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TemplateLocalize allows the Other input to be dynamic
|
||||
func (l *Localizer) TemplateLocalize(ID string, TemplateData map[string]interface{}) string {
|
||||
return l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: ID,
|
||||
},
|
||||
TemplateData: TemplateData,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLanguage returns the currently selected language, e.g 'en'
|
||||
func (l *Localizer) GetLanguage() string {
|
||||
return l.language
|
||||
}
|
||||
|
||||
// add translation file(s)
|
||||
func addBundles(log *logrus.Entry, i18nBundle *i18n.Bundle) {
|
||||
fs := []func(*i18n.Bundle) error{
|
||||
addPolish,
|
||||
addDutch,
|
||||
addEnglish,
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if err := f(i18nBundle); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detectLanguage extracts user language from environment
|
||||
func detectLanguage(langDetector func() (string, error)) string {
|
||||
if userLang, err := langDetector(); err == nil {
|
||||
return userLang
|
||||
}
|
||||
|
||||
return "C"
|
||||
}
|
||||
|
||||
// setupLocalizer creates a new localizer using given userLang
|
||||
func setupLocalizer(log *logrus.Entry, userLang string) *Localizer {
|
||||
// create a i18n bundle that can be used to add translations and other things
|
||||
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
|
||||
|
||||
addBundles(log, i18nBundle)
|
||||
|
||||
// return the new localizer that can be used to translate text
|
||||
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
|
||||
|
||||
return &Localizer{
|
||||
i18nLocalizer: i18nLocalizer,
|
||||
language: userLang,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
87
pkg/i18n/i18n_test.go
Normal file
87
pkg/i18n/i18n_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func getDummyLog() *logrus.Entry {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
return log.WithField("test", "test")
|
||||
}
|
||||
func TestNewLocalizer(t *testing.T) {
|
||||
assert.NotNil(t, NewLocalizer(getDummyLog()))
|
||||
}
|
||||
|
||||
func TestDetectLanguage(t *testing.T) {
|
||||
type scenario struct {
|
||||
langDetector func() (string, error)
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
func() (string, error) {
|
||||
return "", fmt.Errorf("An error occurred")
|
||||
},
|
||||
"C",
|
||||
},
|
||||
{
|
||||
func() (string, error) {
|
||||
return "en", nil
|
||||
},
|
||||
"en",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, detectLanguage(s.langDetector))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalizer(t *testing.T) {
|
||||
type scenario struct {
|
||||
userLang string
|
||||
test func(*Localizer)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"C",
|
||||
func(l *Localizer) {
|
||||
assert.EqualValues(t, "C", l.GetLanguage())
|
||||
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "DiffTitle",
|
||||
},
|
||||
}))
|
||||
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
|
||||
assert.Equal(t, "Are you sure you want to delete the branch test?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
|
||||
},
|
||||
},
|
||||
{
|
||||
"nl",
|
||||
func(l *Localizer) {
|
||||
assert.EqualValues(t, "nl", l.GetLanguage())
|
||||
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: &i18n.Message{
|
||||
ID: "DiffTitle",
|
||||
},
|
||||
}))
|
||||
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
|
||||
assert.Equal(t, "Weet je zeker dat je branch test wil verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(setupLocalizer(getDummyLog(), s.userLang))
|
||||
}
|
||||
}
|
||||
373
pkg/i18n/polish.go
Normal file
373
pkg/i18n/polish.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func addPolish(i18nObject *i18n.Bundle) error {
|
||||
|
||||
return i18nObject.AddMessages(language.Polish,
|
||||
&i18n.Message{
|
||||
ID: "NotEnoughSpace",
|
||||
Other: "Za mało miejsca do wyświetlenia paneli",
|
||||
}, &i18n.Message{
|
||||
ID: "DiffTitle",
|
||||
Other: "Różnice",
|
||||
}, &i18n.Message{
|
||||
ID: "FilesTitle",
|
||||
Other: "Pliki",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchesTitle",
|
||||
Other: "Gałęzie",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commity",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Schowek",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitMessage",
|
||||
Other: "Wiadomość commita",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "commituj zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChangesWithEditor",
|
||||
Other: "commituj zmiany używając edytora z gita",
|
||||
}, &i18n.Message{
|
||||
ID: "StatusTitle",
|
||||
Other: "Status",
|
||||
}, &i18n.Message{
|
||||
ID: "GlobalTitle",
|
||||
Other: "Globalne",
|
||||
}, &i18n.Message{
|
||||
ID: "navigate",
|
||||
Other: "nawiguj",
|
||||
}, &i18n.Message{
|
||||
ID: "menu",
|
||||
Other: "menu",
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "wykonaj",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "przechowaj pliki",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "otwórz",
|
||||
}, &i18n.Message{
|
||||
ID: "ignore",
|
||||
Other: "ignoruj",
|
||||
}, &i18n.Message{
|
||||
ID: "delete",
|
||||
Other: "usuń",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStaged",
|
||||
Other: "przełącz zatwierdzenie",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleStagedAll",
|
||||
Other: "przełącz wszystkie zatwierdzenia",
|
||||
}, &i18n.Message{
|
||||
ID: "refresh",
|
||||
Other: "odśwież",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "dodaj łatkę",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "edytuj",
|
||||
}, &i18n.Message{
|
||||
ID: "scroll",
|
||||
Other: "przewiń",
|
||||
}, &i18n.Message{
|
||||
ID: "abortMerge",
|
||||
Other: "o scalaniu",
|
||||
}, &i18n.Message{
|
||||
ID: "resolveMergeConflicts",
|
||||
Other: "rozwiąż konflikty scalania",
|
||||
}, &i18n.Message{
|
||||
ID: "checkout",
|
||||
Other: "przełącz",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "Brak zmienionych plików",
|
||||
}, &i18n.Message{
|
||||
ID: "FileHasNoUnstagedChanges",
|
||||
Other: "Plik nie zawiera żadnych nieopublikowanych zmian do dodania",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotGitAdd",
|
||||
Other: "Nie można git add --patch nieśledzonych plików",
|
||||
}, &i18n.Message{
|
||||
ID: "CantIgnoreTrackFiles",
|
||||
Other: "Nie można zignorować nieśledzonych plików",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStagedFilesToCommit",
|
||||
Other: "Brak zatwierdzonych plików do commita",
|
||||
}, &i18n.Message{
|
||||
ID: "NoFilesDisplay",
|
||||
Other: "Brak pliku do wyświetlenia",
|
||||
}, &i18n.Message{
|
||||
ID: "PullWait",
|
||||
Other: "Wciąganie zmian...",
|
||||
}, &i18n.Message{
|
||||
ID: "PushWait",
|
||||
Other: "Wypychanie zmian...",
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "Ten plik nie powoduje konfliktów scalania",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD`? Możesz stracić wprowadzone zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
|
||||
}, &i18n.Message{
|
||||
ID: "AlreadyCheckedOutBranch",
|
||||
Other: "Już przęłączono na tą gałąź",
|
||||
}, &i18n.Message{
|
||||
ID: "SureForceCheckout",
|
||||
Other: "Jesteś pewny, że chcesz wymusić przełączenie? Stracisz wszystkie lokalne zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceCheckoutBranch",
|
||||
Other: "Wymuś przełączenie gałęzi",
|
||||
}, &i18n.Message{
|
||||
ID: "BranchName",
|
||||
Other: "Nazwa gałęzi",
|
||||
}, &i18n.Message{
|
||||
ID: "NewBranchNameBranchOff",
|
||||
Other: "Nazwa nowej gałęzi (gałąź na bazie {{.branchName}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CantDeleteCheckOutBranch",
|
||||
Other: "Nie możesz usunąć obecnej przełączonej gałęzi!",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranch",
|
||||
Other: "Usuń gałąź",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteBranchMessage",
|
||||
Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceDeleteBranchMessage",
|
||||
Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?",
|
||||
}, &i18n.Message{
|
||||
ID: "CantMergeBranchIntoItself",
|
||||
Other: "Nie możesz scalić gałęzi do samej siebie",
|
||||
}, &i18n.Message{
|
||||
ID: "forceCheckout",
|
||||
Other: "wymuś przełączenie",
|
||||
}, &i18n.Message{
|
||||
ID: "merge",
|
||||
Other: "scal",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutByName",
|
||||
Other: "przełącz używając nazwy",
|
||||
}, &i18n.Message{
|
||||
ID: "newBranch",
|
||||
Other: "nowa gałąź",
|
||||
}, &i18n.Message{
|
||||
ID: "deleteBranch",
|
||||
Other: "usuń gałąź",
|
||||
}, &i18n.Message{
|
||||
ID: "forceDeleteBranch",
|
||||
Other: "usuń gałąź (wymuś)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchesThisRepo",
|
||||
Other: "Brak gałęzi dla tego repozytorium",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackingThisBranch",
|
||||
Other: "Brak śledzenia dla tej gałęzi",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitWithoutMessageErr",
|
||||
Other: "Nie możesz commitować bez podania wiadomości",
|
||||
}, &i18n.Message{
|
||||
ID: "CloseConfirm",
|
||||
Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
|
||||
}, &i18n.Message{
|
||||
ID: "close",
|
||||
Other: "zamknij",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetThisCommit",
|
||||
Other: "Jesteś pewny, że chcesz zresetować ten commit?",
|
||||
}, &i18n.Message{
|
||||
ID: "ResetToCommit",
|
||||
Other: "Zresetuj, aby commitować",
|
||||
}, &i18n.Message{
|
||||
ID: "squashDown",
|
||||
Other: "ściśnij w dół",
|
||||
}, &i18n.Message{
|
||||
ID: "rename",
|
||||
Other: "przemianuj",
|
||||
}, &i18n.Message{
|
||||
ID: "resetToThisCommit",
|
||||
Other: "zresetuj do tego commita",
|
||||
}, &i18n.Message{
|
||||
ID: "fixupCommit",
|
||||
Other: "napraw commit",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "Brak commitów dla tej gałęzi",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlySquashTopmostCommit",
|
||||
Other: "Można tylko ścisnąć najwyższy commit",
|
||||
}, &i18n.Message{
|
||||
ID: "YouNoCommitsToSquash",
|
||||
Other: "Nie masz commitów do ściśnięcia",
|
||||
}, &i18n.Message{
|
||||
ID: "CantFixupWhileUnstagedChanges",
|
||||
Other: "Nie można wykonać naprawy, kiedy istnieją niezatwierdzone zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "Fixup",
|
||||
Other: "Napraw",
|
||||
}, &i18n.Message{
|
||||
ID: "SureFixupThisCommit",
|
||||
Other: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie ściśnięty w górę wraz z tym",
|
||||
}, &i18n.Message{
|
||||
ID: "OnlyRenameTopCommit",
|
||||
Other: "Można przmianować tylko najwyższy commit",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommit",
|
||||
Other: "przemianuj commit",
|
||||
}, &i18n.Message{
|
||||
ID: "renameCommitEditor",
|
||||
Other: "przemianuj commit w edytorze",
|
||||
}, &i18n.Message{
|
||||
ID: "PotentialErrInGetselectedCommit",
|
||||
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitsThisBranch",
|
||||
Other: "Brak commitów dla tej gałęzi",
|
||||
}, &i18n.Message{
|
||||
ID: "Error",
|
||||
Other: "Błąd",
|
||||
}, &i18n.Message{
|
||||
ID: "resizingPopupPanel",
|
||||
Other: "skalowanie wyskakującego panelu",
|
||||
}, &i18n.Message{
|
||||
ID: "RunningSubprocess",
|
||||
Other: "uruchomiony podproces",
|
||||
}, &i18n.Message{
|
||||
ID: "selectHunk",
|
||||
Other: "wybierz kawałek",
|
||||
}, &i18n.Message{
|
||||
ID: "navigateConflicts",
|
||||
Other: "nawiguj konflikty",
|
||||
}, &i18n.Message{
|
||||
ID: "pickHunk",
|
||||
Other: "wybierz kawałek",
|
||||
}, &i18n.Message{
|
||||
ID: "pickBothHunks",
|
||||
Other: "wybierz oba kawałki",
|
||||
}, &i18n.Message{
|
||||
ID: "undo",
|
||||
Other: "cofnij",
|
||||
}, &i18n.Message{
|
||||
ID: "pop",
|
||||
Other: "wyciągnij",
|
||||
}, &i18n.Message{
|
||||
ID: "drop",
|
||||
Other: "porzuć",
|
||||
}, &i18n.Message{
|
||||
ID: "apply",
|
||||
Other: "zastosuj",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashEntries",
|
||||
Other: "Brak pozycji w schowku",
|
||||
}, &i18n.Message{
|
||||
ID: "StashDrop",
|
||||
Other: "Porzuć schowek",
|
||||
}, &i18n.Message{
|
||||
ID: "SureDropStashEntry",
|
||||
Other: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
|
||||
}, &i18n.Message{
|
||||
ID: "NoStashTo",
|
||||
Other: "Brak schowka dla {{.method}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoTrackedStagedFilesStash",
|
||||
Other: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
|
||||
}, &i18n.Message{
|
||||
ID: "StashChanges",
|
||||
Other: "Przechowaj zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "IssntListOfViews",
|
||||
Other: "{{.name}} nie jest na liście widoków",
|
||||
}, &i18n.Message{
|
||||
ID: "NoViewMachingNewLineFocusedSwitchStatement",
|
||||
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
|
||||
}, &i18n.Message{
|
||||
ID: "settingPreviewsViewTo",
|
||||
Other: "ustawianie poprzedniego widoku na: {{.oldViewName}}",
|
||||
}, &i18n.Message{
|
||||
ID: "newFocusedViewIs",
|
||||
Other: "nowy skupiony widok to {{.newFocusedView}}",
|
||||
}, &i18n.Message{
|
||||
ID: "CantCloseConfirmationPrompt",
|
||||
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "Brak zmienionych plików",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "Wyczyść panel plików",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Scalanie anulowane",
|
||||
}, &i18n.Message{
|
||||
ID: "OpenConfig",
|
||||
Other: "otwórz plik konfiguracyjny",
|
||||
}, &i18n.Message{
|
||||
ID: "EditConfig",
|
||||
Other: "edytuj plik konfiguracyjny",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePush",
|
||||
Other: "Wymuś wypchnięcie",
|
||||
}, &i18n.Message{
|
||||
ID: "ForcePushPrompt",
|
||||
Other: "Twoja gałąź rozeszła się z gałęzią zdalną. Wciśnij 'esc' aby anulować lub 'enter' aby wymusić wypchnięcie.",
|
||||
}, &i18n.Message{
|
||||
ID: "checkForUpdate",
|
||||
Other: "sprawdź aktualizacje",
|
||||
}, &i18n.Message{
|
||||
ID: "CheckingForUpdates",
|
||||
Other: "Sprawdzanie aktualizacji...",
|
||||
}, &i18n.Message{
|
||||
ID: "OnLatestVersionErr",
|
||||
Other: "Już posiadasz najnowszą wersję",
|
||||
}, &i18n.Message{
|
||||
ID: "MajorVersionErr",
|
||||
Other: "Nowa wersja ({{.newVersion}}) posiada niekompatybilne zmiany w porównaniu do obecnej wersji ({{.currentVersion}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CouldNotFindBinaryErr",
|
||||
Other: "Nie można znaleźć pliku binarnego w {{.url}}",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingTitle",
|
||||
Other: "Help make lazygit better",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Włączyć anonimowe raportowanie błędów w celu pomocy w usprawnianiu lazygita (enter/esc)?",
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `usuń jeśli nie śledzony / przełącz jeśli śledzony`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `edytuj plik`,
|
||||
}, &i18n.Message{
|
||||
ID: "openFile",
|
||||
Other: `otwórz plik`,
|
||||
}, &i18n.Message{
|
||||
ID: "ignoreFile",
|
||||
Other: `dodaj do .gitignore`,
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `odśwież pliki`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `zresetuj twardo`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `scal do obecnej gałęzi`,
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Na pewno chcesz wyjść z programu?`,
|
||||
},
|
||||
)
|
||||
}
|
||||
31
pkg/test/test.go
Normal file
31
pkg/test/test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// GenerateRepo generates a repo from test/repos and changes the directory to be
|
||||
// inside the newly made repo
|
||||
func GenerateRepo(filename string) error {
|
||||
reposDir := "/test/repos/"
|
||||
testPath := utils.GetProjectRoot() + reposDir
|
||||
|
||||
// workaround for debian packaging
|
||||
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||||
cwd, _ := os.Getwd()
|
||||
testPath = filepath.Dir(filepath.Dir(cwd)) + reposDir
|
||||
}
|
||||
if err := os.Chdir(testPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
|
||||
return errors.New(string(output))
|
||||
}
|
||||
|
||||
return os.Chdir(testPath + "repo")
|
||||
}
|
||||
314
pkg/updates/updates.go
Normal file
314
pkg/updates/updates.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
|
||||
getter "github.com/jesseduffield/go-getter"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Updater checks for updates and does updates
|
||||
type Updater struct {
|
||||
Log *logrus.Entry
|
||||
Config config.AppConfigurer
|
||||
OSCommand *commands.OSCommand
|
||||
Tr *i18n.Localizer
|
||||
}
|
||||
|
||||
// Updaterer implements the check and update methods
|
||||
type Updaterer interface {
|
||||
CheckForNewUpdate()
|
||||
Update()
|
||||
}
|
||||
|
||||
var (
|
||||
projectUrl = "https://github.com/jesseduffield/lazygit"
|
||||
)
|
||||
|
||||
// NewUpdater creates a new updater
|
||||
func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) {
|
||||
contextLogger := log.WithField("context", "updates")
|
||||
|
||||
updater := &Updater{
|
||||
Log: contextLogger,
|
||||
Config: config,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
}
|
||||
return updater, nil
|
||||
}
|
||||
|
||||
func (u *Updater) getLatestVersionNumber() (string, error) {
|
||||
req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
byt := []byte(body)
|
||||
var dat map[string]interface{}
|
||||
if err := json.Unmarshal(byt, &dat); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dat["tag_name"].(string), nil
|
||||
}
|
||||
|
||||
// RecordLastUpdateCheck records last time an update check was performed
|
||||
func (u *Updater) RecordLastUpdateCheck() error {
|
||||
u.Config.GetAppState().LastUpdateCheck = time.Now().Unix()
|
||||
return u.Config.SaveAppState()
|
||||
}
|
||||
|
||||
// expecting version to be of the form `v12.34.56`
|
||||
func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
|
||||
if oldVersion == "unversioned" {
|
||||
return false
|
||||
}
|
||||
oldVersion = strings.TrimPrefix(oldVersion, "v")
|
||||
newVersion = strings.TrimPrefix(newVersion, "v")
|
||||
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
|
||||
}
|
||||
|
||||
func (u *Updater) checkForNewUpdate() (string, error) {
|
||||
u.Log.Info("Checking for an updated version")
|
||||
currentVersion := u.Config.GetVersion()
|
||||
if err := u.RecordLastUpdateCheck(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newVersion, err := u.getLatestVersionNumber()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Log.Info("Current version is " + currentVersion)
|
||||
u.Log.Info("New version is " + newVersion)
|
||||
|
||||
if newVersion == currentVersion {
|
||||
return "", errors.New(u.Tr.SLocalize("OnLatestVersionErr"))
|
||||
}
|
||||
|
||||
if u.majorVersionDiffers(currentVersion, newVersion) {
|
||||
errMessage := u.Tr.TemplateLocalize(
|
||||
"MajorVersionErr",
|
||||
i18n.Teml{
|
||||
"newVersion": newVersion,
|
||||
"currentVersion": currentVersion,
|
||||
},
|
||||
)
|
||||
return "", errors.New(errMessage)
|
||||
}
|
||||
|
||||
rawUrl, err := u.getBinaryUrl(newVersion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Log.Info("Checking for resource at url " + rawUrl)
|
||||
if !u.verifyResourceFound(rawUrl) {
|
||||
errMessage := u.Tr.TemplateLocalize(
|
||||
"CouldNotFindBinaryErr",
|
||||
i18n.Teml{
|
||||
"url": rawUrl,
|
||||
},
|
||||
)
|
||||
return "", errors.New(errMessage)
|
||||
}
|
||||
u.Log.Info("Verified resource is available, ready to update")
|
||||
|
||||
return newVersion, nil
|
||||
}
|
||||
|
||||
// CheckForNewUpdate checks if there is an available update
|
||||
func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) {
|
||||
if !userRequested && u.skipUpdateCheck() {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
newVersion, err := u.checkForNewUpdate()
|
||||
if err = onFinish(newVersion, err); err != nil {
|
||||
u.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *Updater) skipUpdateCheck() bool {
|
||||
// will remove the check for windows after adding a manifest file asking for
|
||||
// the required permissions
|
||||
if runtime.GOOS == "windows" {
|
||||
u.Log.Info("Updating is currently not supported for windows until we can fix permission issues")
|
||||
return true
|
||||
}
|
||||
|
||||
if u.Config.GetVersion() == "unversioned" {
|
||||
u.Log.Info("Current version is not built from an official release so we won't check for an update")
|
||||
return true
|
||||
}
|
||||
|
||||
if u.Config.GetBuildSource() != "buildBinary" {
|
||||
u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update")
|
||||
return true
|
||||
}
|
||||
|
||||
userConfig := u.Config.GetUserConfig()
|
||||
if userConfig.Get("update.method") == "never" {
|
||||
u.Log.Info("Update method is set to never so we won't check for an update")
|
||||
return true
|
||||
}
|
||||
|
||||
currentTimestamp := time.Now().Unix()
|
||||
lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck
|
||||
days := userConfig.GetInt64("update.days")
|
||||
|
||||
if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days {
|
||||
u.Log.Info("Last update was too recent so we won't check for an update")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *Updater) mappedOs(os string) string {
|
||||
osMap := map[string]string{
|
||||
"darwin": "Darwin",
|
||||
"linux": "Linux",
|
||||
"windows": "Windows",
|
||||
}
|
||||
result, found := osMap[os]
|
||||
if found {
|
||||
return result
|
||||
}
|
||||
return os
|
||||
}
|
||||
|
||||
func (u *Updater) mappedArch(arch string) string {
|
||||
archMap := map[string]string{
|
||||
"386": "32-bit",
|
||||
"amd64": "x86_64",
|
||||
}
|
||||
result, found := archMap[arch]
|
||||
if found {
|
||||
return result
|
||||
}
|
||||
return arch
|
||||
}
|
||||
|
||||
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
|
||||
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
|
||||
extension := "tar.gz"
|
||||
if runtime.GOOS == "windows" {
|
||||
extension = "zip"
|
||||
}
|
||||
url := fmt.Sprintf(
|
||||
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
|
||||
projectUrl,
|
||||
newVersion,
|
||||
newVersion[1:],
|
||||
u.mappedOs(runtime.GOOS),
|
||||
u.mappedArch(runtime.GOARCH),
|
||||
extension,
|
||||
)
|
||||
u.Log.Info("url for latest release is " + url)
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// Update downloads the latest binary and replaces the current binary with it
|
||||
func (u *Updater) Update(newVersion string, onFinish func(error) error) {
|
||||
go func() {
|
||||
err := u.update(newVersion)
|
||||
if err = onFinish(err); err != nil {
|
||||
u.Log.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (u *Updater) update(newVersion string) error {
|
||||
rawUrl, err := u.getBinaryUrl(newVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Log.Info("updating with url " + rawUrl)
|
||||
return u.downloadAndInstall(rawUrl)
|
||||
}
|
||||
|
||||
func (u *Updater) downloadAndInstall(rawUrl string) error {
|
||||
url, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g := new(getter.HttpGetter)
|
||||
tempDir, err := ioutil.TempDir("", "lazygit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
u.Log.Info("temp directory is " + tempDir)
|
||||
|
||||
// Get it!
|
||||
if err := g.Get(tempDir, url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the path of the current binary
|
||||
binaryPath, err := osext.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Log.Info("binary path is " + binaryPath)
|
||||
|
||||
binaryName := filepath.Base(binaryPath)
|
||||
u.Log.Info("binary name is " + binaryName)
|
||||
|
||||
// Verify the main file exists
|
||||
tempPath := filepath.Join(tempDir, binaryName)
|
||||
u.Log.Info("temp path to binary is " + tempPath)
|
||||
if _, err := os.Stat(tempPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap out the old binary for the new one
|
||||
err = os.Rename(tempPath, binaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Log.Info("update complete!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Updater) verifyResourceFound(rawUrl string) bool {
|
||||
resp, err := http.Head(rawUrl)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
u.Log.Info("Received status code ", resp.StatusCode)
|
||||
// 403 means the resource is there (not going to bother adding extra request headers)
|
||||
// 404 means its not
|
||||
return resp.StatusCode == 403
|
||||
}
|
||||
101
pkg/utils/utils.go
Normal file
101
pkg/utils/utils.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// SplitLines takes a multiline string and splits it on newlines
|
||||
// currently we are also stripping \r's which may have adverse effects for
|
||||
// windows users (but no issues have been raised yet)
|
||||
func SplitLines(multilineString string) []string {
|
||||
multilineString = strings.Replace(multilineString, "\r", "", -1)
|
||||
if multilineString == "" || multilineString == "\n" {
|
||||
return make([]string, 0)
|
||||
}
|
||||
lines := strings.Split(multilineString, "\n")
|
||||
if lines[len(lines)-1] == "" {
|
||||
return lines[:len(lines)-1]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// WithPadding pads a string as much as you want
|
||||
func WithPadding(str string, padding int) string {
|
||||
if padding-len(str) < 0 {
|
||||
return str
|
||||
}
|
||||
return str + strings.Repeat(" ", padding-len(str))
|
||||
}
|
||||
|
||||
// ColoredString takes a string and a colour attribute and returns a colored
|
||||
// string with that attribute
|
||||
func ColoredString(str string, colorAttribute color.Attribute) string {
|
||||
colour := color.New(colorAttribute)
|
||||
return ColoredStringDirect(str, colour)
|
||||
}
|
||||
|
||||
// ColoredStringDirect used for aggregating a few color attributes rather than
|
||||
// just sending a single one
|
||||
func ColoredStringDirect(str string, colour *color.Color) string {
|
||||
return colour.SprintFunc()(fmt.Sprint(str))
|
||||
}
|
||||
|
||||
// GetCurrentRepoName gets the repo's base name
|
||||
func GetCurrentRepoName() string {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
return filepath.Base(pwd)
|
||||
}
|
||||
|
||||
// TrimTrailingNewline - Trims the trailing newline
|
||||
// TODO: replace with `chomp` after refactor
|
||||
func TrimTrailingNewline(str string) string {
|
||||
if strings.HasSuffix(str, "\n") {
|
||||
return str[:len(str)-1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
|
||||
func NormalizeLinefeeds(str string) string {
|
||||
str = strings.Replace(str, "\r\n", "\n", -1)
|
||||
str = strings.Replace(str, "\r", "", -1)
|
||||
return str
|
||||
}
|
||||
|
||||
// GetProjectRoot returns the path to the root of the project. Only to be used
|
||||
// in testing contexts, as with binaries it's unlikely this path will exist on
|
||||
// the machine
|
||||
func GetProjectRoot() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.Split(dir, "lazygit")[0] + "lazygit"
|
||||
}
|
||||
|
||||
// Loader dumps a string to be displayed as a loader
|
||||
func Loader() string {
|
||||
characters := "|/-\\"
|
||||
now := time.Now()
|
||||
nanos := now.UnixNano()
|
||||
index := nanos / 50000000 % int64(len(characters))
|
||||
return characters[index : index+1]
|
||||
}
|
||||
|
||||
// ResolvePlaceholderString populates a template with values
|
||||
func ResolvePlaceholderString(str string, arguments map[string]string) string {
|
||||
for key, value := range arguments {
|
||||
str = strings.Replace(str, "{{"+key+"}}", value, -1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
169
pkg/utils/utils_test.go
Normal file
169
pkg/utils/utils_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSplitLines(t *testing.T) {
|
||||
type scenario struct {
|
||||
multilineString string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"",
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"\n",
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"hello world !\nhello universe !\n",
|
||||
[]string{
|
||||
"hello world !",
|
||||
"hello universe !",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, SplitLines(s.multilineString))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithPadding(t *testing.T) {
|
||||
type scenario struct {
|
||||
str string
|
||||
padding int
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"hello world !",
|
||||
1,
|
||||
"hello world !",
|
||||
},
|
||||
{
|
||||
"hello world !",
|
||||
14,
|
||||
"hello world ! ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimTrailingNewline(t *testing.T) {
|
||||
type scenario struct {
|
||||
str string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"hello world !\n",
|
||||
"hello world !",
|
||||
},
|
||||
{
|
||||
"hello world !",
|
||||
"hello world !",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLinefeeds(t *testing.T) {
|
||||
type scenario struct {
|
||||
byteArray []byte
|
||||
expected []byte
|
||||
}
|
||||
var scenarios = []scenario{
|
||||
{
|
||||
// \r\n
|
||||
[]byte{97, 115, 100, 102, 13, 10},
|
||||
[]byte{97, 115, 100, 102, 10},
|
||||
},
|
||||
{
|
||||
// bash\r\nblah
|
||||
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
|
||||
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
|
||||
},
|
||||
{
|
||||
// \r
|
||||
[]byte{97, 115, 100, 102, 13},
|
||||
[]byte{97, 115, 100, 102},
|
||||
},
|
||||
{
|
||||
// \n
|
||||
[]byte{97, 115, 100, 102, 10},
|
||||
[]byte{97, 115, 100, 102, 10},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePlaceholderString(t *testing.T) {
|
||||
type scenario struct {
|
||||
templateString string
|
||||
arguments map[string]string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"",
|
||||
map[string]string{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"hello",
|
||||
map[string]string{},
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"hello {{arg}}",
|
||||
map[string]string{},
|
||||
"hello {{arg}}",
|
||||
},
|
||||
{
|
||||
"hello {{arg}}",
|
||||
map[string]string{"arg": "there"},
|
||||
"hello there",
|
||||
},
|
||||
{
|
||||
"hello",
|
||||
map[string]string{"arg": "there"},
|
||||
"hello",
|
||||
},
|
||||
{
|
||||
"{{nothing}}",
|
||||
map[string]string{"nothing": ""},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
|
||||
map[string]string{
|
||||
"blah": "blah",
|
||||
"this": "won't match",
|
||||
},
|
||||
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
|
||||
}
|
||||
}
|
||||
54
scripts/generate_cheatsheet.go
Normal file
54
scripts/generate_cheatsheet.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// run:
|
||||
// LANG=en go run generate_cheatsheet.go
|
||||
// to generate Keybindings_en.md file in current directory
|
||||
// change LANG to generate cheatsheet in different language (if supported)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
appConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
|
||||
a, _ := app.NewApp(appConfig)
|
||||
lang := a.Tr.GetLanguage()
|
||||
name := "Keybindings_" + lang + ".md"
|
||||
bindings := a.Gui.GetKeybindings()
|
||||
padWidth := a.Gui.GetMaxKeyLength(bindings)
|
||||
file, _ := os.Create(name)
|
||||
current := "v"
|
||||
content := ""
|
||||
title := ""
|
||||
|
||||
file.WriteString("# Lazygit " + a.Tr.SLocalize("menu"))
|
||||
|
||||
for _, binding := range bindings {
|
||||
if key := a.Gui.GetKey(binding); key != "" && (binding.Description != "" || key == "x") {
|
||||
if binding.ViewName != current {
|
||||
current = binding.ViewName
|
||||
if current == "" {
|
||||
title = a.Tr.SLocalize("GlobalTitle")
|
||||
} else {
|
||||
title = a.Tr.SLocalize(strings.Title(current) + "Title")
|
||||
}
|
||||
content = fmt.Sprintf("</pre>\n\n## %s\n<pre>\n", title)
|
||||
file.WriteString(content)
|
||||
}
|
||||
// workaround to include menu keybinding in cheatsheet
|
||||
// could not add this Description field directly to keybindings.go,
|
||||
// because then menu key would be displayed in menu itself and that is undesirable
|
||||
if key == "x" {
|
||||
binding.Description = a.Tr.SLocalize("menu")
|
||||
}
|
||||
content = fmt.Sprintf("\t<kbd>%s</kbd>%s %s\n", key, strings.TrimPrefix(utils.WithPadding(key, padWidth), key), binding.Description)
|
||||
file.WriteString(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
63
scripts/push_new_patch.go
Executable file
63
scripts/push_new_patch.go
Executable file
@@ -0,0 +1,63 @@
|
||||
// call from project root with
|
||||
// go run bin/push_new_patch.go
|
||||
|
||||
// goreleaser expects a $GITHUB_TOKEN env variable to be defined
|
||||
// in order to push the release got github
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
version, err := ioutil.ReadFile("VERSION")
|
||||
if err != nil {
|
||||
log.Panicln(err.Error())
|
||||
}
|
||||
stringVersion := string(version)
|
||||
fmt.Println("VERSION was " + stringVersion)
|
||||
|
||||
runCommand("git", "pull")
|
||||
|
||||
splitVersion := strings.Split(stringVersion, ".")
|
||||
patch := splitVersion[len(splitVersion)-1]
|
||||
newPatch, err := strconv.Atoi(patch)
|
||||
splitVersion[len(splitVersion)-1] = strconv.FormatInt(int64(newPatch)+1, 10)
|
||||
newVersion := strings.Join(splitVersion, ".")
|
||||
|
||||
err = ioutil.WriteFile("VERSION", []byte(newVersion), 0644)
|
||||
if err != nil {
|
||||
log.Panicln(err.Error())
|
||||
}
|
||||
|
||||
runCommand("git", "add", "VERSION")
|
||||
runCommand("git", "commit", "-m", "bump version to "+newVersion, "--", "VERSION")
|
||||
runCommand("git", "push")
|
||||
runCommand("git", "tag", newVersion)
|
||||
runCommand("git", "push", "origin", newVersion)
|
||||
runCommand("goreleaser", "--rm-dist")
|
||||
runCommand("rm", "-rf", "dist")
|
||||
}
|
||||
|
||||
func runCommand(args ...string) {
|
||||
fmt.Println(strings.Join(args, " "))
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func refreshStashEntries(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
v, err := g.View("stash")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
state.StashEntries = getGitStashEntries()
|
||||
v.Clear()
|
||||
for _, stashEntry := range state.StashEntries {
|
||||
fmt.Fprintln(v, stashEntry.DisplayString)
|
||||
}
|
||||
return resetOrigin(v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSelectedStashEntry(v *gocui.View) *StashEntry {
|
||||
if len(state.StashEntries) == 0 {
|
||||
return nil
|
||||
}
|
||||
lineNumber := getItemPosition(v)
|
||||
return &state.StashEntries[lineNumber]
|
||||
}
|
||||
|
||||
func renderStashOptions(g *gocui.Gui) error {
|
||||
return renderOptionsMap(g, map[string]string{
|
||||
"space": "apply",
|
||||
"k": "pop",
|
||||
"d": "drop",
|
||||
"← → ↑ ↓": "navigate",
|
||||
})
|
||||
}
|
||||
|
||||
func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := renderStashOptions(g); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
stashEntry := getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
renderString(g, "main", "No stash entries")
|
||||
return
|
||||
}
|
||||
diff, _ := getStashEntryDiff(stashEntry.Index)
|
||||
renderString(g, "main", diff)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
return stashDo(g, v, "apply")
|
||||
}
|
||||
|
||||
func handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
return stashDo(g, v, "pop")
|
||||
}
|
||||
|
||||
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return stashDo(g, v, "drop")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
stashEntry := getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
return createErrorPanel(g, "No stash to "+method)
|
||||
}
|
||||
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
}
|
||||
refreshStashEntries(g)
|
||||
return refreshFiles(g)
|
||||
}
|
||||
|
||||
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitStashSave(trimmedContent(v)); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
}
|
||||
refreshStashEntries(g)
|
||||
return refreshFiles(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func refreshStatus(g *gocui.Gui) error {
|
||||
v, err := g.View("status")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// for some reason if this isn't wrapped in an update the clear seems to
|
||||
// be applied after the other things or something like that; the panel's
|
||||
// contents end up cleared
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v.Clear()
|
||||
pushables, pullables := gitUpstreamDifferenceCount()
|
||||
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
||||
branches := state.Branches
|
||||
if err := updateHasMergeConflictStatus(); err != nil {
|
||||
return err
|
||||
}
|
||||
if state.HasMergeConflicts {
|
||||
colour := color.New(color.FgYellow)
|
||||
fmt.Fprint(v, coloredString(" (merging)", colour))
|
||||
}
|
||||
if len(branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
branch := branches[0]
|
||||
// utilising the fact these all have padding to only grab the name
|
||||
// from the display string with the existing coloring applied
|
||||
fmt.Fprint(v, " "+branch.DisplayString[4:])
|
||||
colorLog(color.FgCyan, time.Now().Sub(startTime))
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
14
test.sh
Executable file
14
test.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $( find ./* -maxdepth 10 ! -path "./vendor*" ! -path "./.git*" ! -path "./scripts*" -type d); do
|
||||
if ls $d/*.go &> /dev/null; then
|
||||
go test -v -race -coverprofile=profile.out -covermode=atomic $d
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
fi
|
||||
done
|
||||
23
test/repos/bom.sh
Executable file
23
test/repos/bom.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
|
||||
cat <<EOT >> windowslf.txt
|
||||
asdf
|
||||
asdf
|
||||
EOT
|
||||
|
||||
cat <<EOT >> linuxlf.txt
|
||||
asdf
|
||||
asdf
|
||||
EOT
|
||||
|
||||
cat <<EOT >> bomtest.txt
|
||||
A,B,C,D,E
|
||||
F,G,H,I,J
|
||||
K,L,M,N,O
|
||||
P,Q,R,S,T
|
||||
U,V,W,X,Y
|
||||
Z,1,2,3,4
|
||||
EOT
|
||||
19
test/repos/case_insensitive_checkouts.sh
Executable file
19
test/repos/case_insensitive_checkouts.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
touch foo
|
||||
git add foo
|
||||
git commit -m "init"
|
||||
git branch -a
|
||||
git branch test
|
||||
git branch TEST
|
||||
git checkout TEST
|
||||
git checkout TeST
|
||||
git checkout TesT
|
||||
git checkout TEsT
|
||||
git branch -a
|
||||
7
test/repos/extras/pre-commit
Normal file
7
test/repos/extras/pre-commit
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "test1"
|
||||
sleep 1
|
||||
echo "test2"
|
||||
sleep 1
|
||||
echo "test3"
|
||||
19
test/repos/gpg.sh
Executable file
19
test/repos/gpg.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
git config gpg.program $(which gpg)
|
||||
git config user.signingkey E304229F # test key
|
||||
git config commit.gpgsign true
|
||||
git config credential.helper store
|
||||
git config credential.helper cache 1
|
||||
|
||||
touch foo
|
||||
git add foo
|
||||
|
||||
touch bar
|
||||
git add bar
|
||||
19
test/repos/lots_of_commits.sh
Executable file
19
test/repos/lots_of_commits.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
i=2
|
||||
end=100
|
||||
while [ $i -le $end ]; do
|
||||
echo "file${i}" > file${i}
|
||||
git add file${i}
|
||||
git commit -m file${i}
|
||||
|
||||
i=$(($i+1))
|
||||
done
|
||||
|
||||
echo "unstaged change" > file100
|
||||
35
test/repos/lots_of_diffs.sh
Executable file
35
test/repos/lots_of_diffs.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
echo "deleted" > deleted_staged
|
||||
echo "deleted_unstaged" > deleted_unstaged
|
||||
echo "modified_staged" > modified_staged
|
||||
echo "modified_unstaged" > modified_unstaged
|
||||
echo "renamed" > renamed_before
|
||||
|
||||
git add .
|
||||
git commit -m "files to delete"
|
||||
rm deleted_staged
|
||||
rm deleted_unstaged
|
||||
|
||||
rm renamed_before
|
||||
echo "renamed" > renamed_after
|
||||
echo "more" >> modified_staged
|
||||
echo "more" >> modified_unstaged
|
||||
echo "untracked_staged" > untracked_staged
|
||||
echo "untracked_unstaged" > untracked_unstaged
|
||||
echo "blah" > "file with space staged"
|
||||
echo "blah" > "file with space unstaged"
|
||||
echo "same name as branch" > master
|
||||
|
||||
git add deleted_staged
|
||||
git add modified_staged
|
||||
git add untracked_staged
|
||||
git add "file with space staged"
|
||||
git add renamed_before
|
||||
git add renamed_after
|
||||
@@ -1,22 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# this script will make a repo with a master and develop branch, where we end up
|
||||
# on the master branch and if we try and merge master we get a merge conflict
|
||||
|
||||
# call this command from the test directory:
|
||||
# ./generate_basic_repo.sh; cd testrepo; gg; cd ..
|
||||
|
||||
# -e means exit if something fails
|
||||
# -x means print out simple commands before running them
|
||||
set -ex
|
||||
|
||||
reponame="testrepo"
|
||||
|
||||
rm -rf ${reponame}
|
||||
mkdir ${reponame}
|
||||
cd ${reponame}
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
function add_spacing {
|
||||
for i in {1..60}
|
||||
@@ -25,9 +13,15 @@ function add_spacing {
|
||||
done
|
||||
}
|
||||
|
||||
mkdir directory
|
||||
echo "test1" > directory/file
|
||||
echo "test1" > directory/file2
|
||||
|
||||
|
||||
echo "Here is a story that has been told throuhg the ages" >> file1
|
||||
|
||||
git add file1
|
||||
git add directory
|
||||
git commit -m "first commit"
|
||||
|
||||
git checkout -b develop
|
||||
@@ -36,6 +30,11 @@ echo "once upon a time there was a dog" >> file1
|
||||
add_spacing file1
|
||||
echo "once upon a time there was another dog" >> file1
|
||||
git add file1
|
||||
|
||||
echo "test2" > directory/file
|
||||
echo "test2" > directory/file2
|
||||
git add directory
|
||||
|
||||
git commit -m "first commit on develop"
|
||||
|
||||
git checkout master
|
||||
@@ -44,6 +43,11 @@ echo "once upon a time there was a cat" >> file1
|
||||
add_spacing file1
|
||||
echo "once upon a time there was another cat" >> file1
|
||||
git add file1
|
||||
|
||||
echo "test3" > directory/file
|
||||
echo "test3" > directory/file2
|
||||
git add directory
|
||||
|
||||
git commit -m "first commit on develop"
|
||||
|
||||
git merge develop # should have a merge conflict here
|
||||
12
test/repos/pre_commit_hook.sh
Executable file
12
test/repos/pre_commit_hook.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
cp ../extras/pre-commit .git/hooks/pre-commit
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
echo "file" > file
|
||||
git add file
|
||||
22
test/repos/unicode_characters.sh
Executable file
22
test/repos/unicode_characters.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
set -ex; rm -rf repo; mkdir repo; cd repo
|
||||
|
||||
git init
|
||||
git config user.email "test@example.com"
|
||||
git config user.name "Lazygit Tester"
|
||||
|
||||
|
||||
# Add some ansi, unicode, zero width joiner caracters
|
||||
cat <<EOT >> charstest.txt
|
||||
ANSI Œ (U+0152 Œ Latin capital ligature OE Latin Extended-A)
|
||||
¥ (0xA5 U+00A5 ¥ yes sign)
|
||||
ƒ (0x83 U+0192 ƒ Latin small letter f with hook)
|
||||
ZWJ https://en.wikipedia.org/wiki/Zero-width_joiner / https://unicode.org/Public/emoji/4.0/emoji-zwj-sequences.txt 👶(👨👦)
|
||||
UNICODE ☆ 🤓 え 术
|
||||
EOT
|
||||
git add charstest.txt
|
||||
git commit -m "Test chars Œ¥ƒ👶👨👦☆ 🤓 え 术👩💻👩🏻💻👩🏽💻👩🏼💻👩🏾💻👩🏿💻👨💻👨🏻💻👨🏼💻👨🏽💻👨🏾💻👨🏿💻 commit"
|
||||
echo "我喜歡編碼" >> charstest.txt
|
||||
echo "நான் குறியீடு விரும்புகிறேன்" >> charstest.txt
|
||||
git add charstest.txt
|
||||
git commit -m "Test chars 我喜歡編碼 நான் குறியீடு விரும்புகிறேன் commit"
|
||||
202
vendor/github.com/aws/aws-sdk-go/LICENSE.txt
generated
vendored
Normal file
202
vendor/github.com/aws/aws-sdk-go/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
3
vendor/github.com/aws/aws-sdk-go/NOTICE.txt
generated
vendored
Normal file
3
vendor/github.com/aws/aws-sdk-go/NOTICE.txt
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
AWS SDK for Go
|
||||
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Copyright 2014-2015 Stripe, Inc.
|
||||
145
vendor/github.com/aws/aws-sdk-go/aws/awserr/error.go
generated
vendored
Normal file
145
vendor/github.com/aws/aws-sdk-go/aws/awserr/error.go
generated
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
// Package awserr represents API error interface accessors for the SDK.
|
||||
package awserr
|
||||
|
||||
// An Error wraps lower level errors with code, message and an original error.
|
||||
// The underlying concrete error type may also satisfy other interfaces which
|
||||
// can be to used to obtain more specific information about the error.
|
||||
//
|
||||
// Calling Error() or String() will always include the full information about
|
||||
// an error based on its underlying type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Get error details
|
||||
// log.Println("Error:", awsErr.Code(), awsErr.Message())
|
||||
//
|
||||
// // Prints out full error message, including original error if there was one.
|
||||
// log.Println("Error:", awsErr.Error())
|
||||
//
|
||||
// // Get original error
|
||||
// if origErr := awsErr.OrigErr(); origErr != nil {
|
||||
// // operate on original error.
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type Error interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErr() error
|
||||
}
|
||||
|
||||
// BatchError is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
|
||||
// compatibility.
|
||||
type BatchError interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// BatchedErrors is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Replaces BatchError
|
||||
type BatchedErrors interface {
|
||||
// Satisfy the base Error interface.
|
||||
Error
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// New returns an Error object described by the code, message, and origErr.
|
||||
//
|
||||
// If origErr satisfies the Error interface it will not be wrapped within a new
|
||||
// Error object and will instead be returned.
|
||||
func New(code, message string, origErr error) Error {
|
||||
var errs []error
|
||||
if origErr != nil {
|
||||
errs = append(errs, origErr)
|
||||
}
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// NewBatchError returns an BatchedErrors with a collection of errors as an
|
||||
// array of errors.
|
||||
func NewBatchError(code, message string, errs []error) BatchedErrors {
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// A RequestFailure is an interface to extract request failure information from
|
||||
// an Error such as the request ID of the failed request returned by a service.
|
||||
// RequestFailures may not always have a requestID value if the request failed
|
||||
// prior to reaching the service such as a connection error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if reqerr, ok := err.(RequestFailure); ok {
|
||||
// log.Println("Request failed", reqerr.Code(), reqerr.Message(), reqerr.RequestID())
|
||||
// } else {
|
||||
// log.Println("Error:", err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Combined with awserr.Error:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Generic AWS Error with Code, Message, and original error (if any)
|
||||
// fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr())
|
||||
//
|
||||
// if reqErr, ok := err.(awserr.RequestFailure); ok {
|
||||
// // A service error occurred
|
||||
// fmt.Println(reqErr.StatusCode(), reqErr.RequestID())
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type RequestFailure interface {
|
||||
Error
|
||||
|
||||
// The status code of the HTTP response.
|
||||
StatusCode() int
|
||||
|
||||
// The request ID returned by the service for a request failure. This will
|
||||
// be empty if no request ID is available such as the request failed due
|
||||
// to a connection error.
|
||||
RequestID() string
|
||||
}
|
||||
|
||||
// NewRequestFailure returns a new request error wrapper for the given Error
|
||||
// provided.
|
||||
func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure {
|
||||
return newRequestError(err, statusCode, reqID)
|
||||
}
|
||||
194
vendor/github.com/aws/aws-sdk-go/aws/awserr/types.go
generated
vendored
Normal file
194
vendor/github.com/aws/aws-sdk-go/aws/awserr/types.go
generated
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
package awserr
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SprintError returns a string of the formatted error code.
|
||||
//
|
||||
// Both extra and origErr are optional. If they are included their lines
|
||||
// will be added, but if they are not included their lines will be ignored.
|
||||
func SprintError(code, message, extra string, origErr error) string {
|
||||
msg := fmt.Sprintf("%s: %s", code, message)
|
||||
if extra != "" {
|
||||
msg = fmt.Sprintf("%s\n\t%s", msg, extra)
|
||||
}
|
||||
if origErr != nil {
|
||||
msg = fmt.Sprintf("%s\ncaused by: %s", msg, origErr.Error())
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// A baseError wraps the code and message which defines an error. It also
|
||||
// can be used to wrap an original error object.
|
||||
//
|
||||
// Should be used as the root for errors satisfying the awserr.Error. Also
|
||||
// for any error which does not fit into a specific error wrapper type.
|
||||
type baseError struct {
|
||||
// Classification of error
|
||||
code string
|
||||
|
||||
// Detailed information about error
|
||||
message string
|
||||
|
||||
// Optional original error this error is based off of. Allows building
|
||||
// chained errors.
|
||||
errs []error
|
||||
}
|
||||
|
||||
// newBaseError returns an error object for the code, message, and errors.
|
||||
//
|
||||
// code is a short no whitespace phrase depicting the classification of
|
||||
// the error that is being created.
|
||||
//
|
||||
// message is the free flow string containing detailed information about the
|
||||
// error.
|
||||
//
|
||||
// origErrs is the error objects which will be nested under the new errors to
|
||||
// be returned.
|
||||
func newBaseError(code, message string, origErrs []error) *baseError {
|
||||
b := &baseError{
|
||||
code: code,
|
||||
message: message,
|
||||
errs: origErrs,
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// See ErrorWithExtra for formatting.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (b baseError) Error() string {
|
||||
size := len(b.errs)
|
||||
if size > 0 {
|
||||
return SprintError(b.code, b.message, "", errorList(b.errs))
|
||||
}
|
||||
|
||||
return SprintError(b.code, b.message, "", nil)
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (b baseError) String() string {
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
// Code returns the short phrase depicting the classification of the error.
|
||||
func (b baseError) Code() string {
|
||||
return b.code
|
||||
}
|
||||
|
||||
// Message returns the error details message.
|
||||
func (b baseError) Message() string {
|
||||
return b.message
|
||||
}
|
||||
|
||||
// OrigErr returns the original error if one was set. Nil is returned if no
|
||||
// error was set. This only returns the first element in the list. If the full
|
||||
// list is needed, use BatchedErrors.
|
||||
func (b baseError) OrigErr() error {
|
||||
switch len(b.errs) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return b.errs[0]
|
||||
default:
|
||||
if err, ok := b.errs[0].(Error); ok {
|
||||
return NewBatchError(err.Code(), err.Message(), b.errs[1:])
|
||||
}
|
||||
return NewBatchError("BatchedErrors",
|
||||
"multiple errors occurred", b.errs)
|
||||
}
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (b baseError) OrigErrs() []error {
|
||||
return b.errs
|
||||
}
|
||||
|
||||
// So that the Error interface type can be included as an anonymous field
|
||||
// in the requestError struct and not conflict with the error.Error() method.
|
||||
type awsError Error
|
||||
|
||||
// A requestError wraps a request or service error.
|
||||
//
|
||||
// Composed of baseError for code, message, and original error.
|
||||
type requestError struct {
|
||||
awsError
|
||||
statusCode int
|
||||
requestID string
|
||||
}
|
||||
|
||||
// newRequestError returns a wrapped error with additional information for
|
||||
// request status code, and service requestID.
|
||||
//
|
||||
// Should be used to wrap all request which involve service requests. Even if
|
||||
// the request failed without a service response, but had an HTTP status code
|
||||
// that may be meaningful.
|
||||
//
|
||||
// Also wraps original errors via the baseError.
|
||||
func newRequestError(err Error, statusCode int, requestID string) *requestError {
|
||||
return &requestError{
|
||||
awsError: err,
|
||||
statusCode: statusCode,
|
||||
requestID: requestID,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
// Satisfies the error interface.
|
||||
func (r requestError) Error() string {
|
||||
extra := fmt.Sprintf("status code: %d, request id: %s",
|
||||
r.statusCode, r.requestID)
|
||||
return SprintError(r.Code(), r.Message(), extra, r.OrigErr())
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (r requestError) String() string {
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// StatusCode returns the wrapped status code for the error
|
||||
func (r requestError) StatusCode() int {
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
// RequestID returns the wrapped requestID
|
||||
func (r requestError) RequestID() string {
|
||||
return r.requestID
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (r requestError) OrigErrs() []error {
|
||||
if b, ok := r.awsError.(BatchedErrors); ok {
|
||||
return b.OrigErrs()
|
||||
}
|
||||
return []error{r.OrigErr()}
|
||||
}
|
||||
|
||||
// An error list that satisfies the golang interface
|
||||
type errorList []error
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (e errorList) Error() string {
|
||||
msg := ""
|
||||
// How do we want to handle the array size being zero
|
||||
if size := len(e); size > 0 {
|
||||
for i := 0; i < size; i++ {
|
||||
msg += fmt.Sprintf("%s", e[i].Error())
|
||||
// We check the next index to see if it is within the slice.
|
||||
// If it is, then we append a newline. We do this, because unit tests
|
||||
// could be broken with the additional '\n'
|
||||
if i+1 < size {
|
||||
msg += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
108
vendor/github.com/aws/aws-sdk-go/aws/awsutil/copy.go
generated
vendored
Normal file
108
vendor/github.com/aws/aws-sdk-go/aws/awsutil/copy.go
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Copy deeply copies a src structure to dst. Useful for copying request and
|
||||
// response structures.
|
||||
//
|
||||
// Can copy between structs of different type, but will only copy fields which
|
||||
// are assignable, and exist in both structs. Fields which are not assignable,
|
||||
// or do not exist in both structs are ignored.
|
||||
func Copy(dst, src interface{}) {
|
||||
dstval := reflect.ValueOf(dst)
|
||||
if !dstval.IsValid() {
|
||||
panic("Copy dst cannot be nil")
|
||||
}
|
||||
|
||||
rcopy(dstval, reflect.ValueOf(src), true)
|
||||
}
|
||||
|
||||
// CopyOf returns a copy of src while also allocating the memory for dst.
|
||||
// src must be a pointer type or this operation will fail.
|
||||
func CopyOf(src interface{}) (dst interface{}) {
|
||||
dsti := reflect.New(reflect.TypeOf(src).Elem())
|
||||
dst = dsti.Interface()
|
||||
rcopy(dsti, reflect.ValueOf(src), true)
|
||||
return
|
||||
}
|
||||
|
||||
// rcopy performs a recursive copy of values from the source to destination.
|
||||
//
|
||||
// root is used to skip certain aspects of the copy which are not valid
|
||||
// for the root node of a object.
|
||||
func rcopy(dst, src reflect.Value, root bool) {
|
||||
if !src.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
switch src.Kind() {
|
||||
case reflect.Ptr:
|
||||
if _, ok := src.Interface().(io.Reader); ok {
|
||||
if dst.Kind() == reflect.Ptr && dst.Elem().CanSet() {
|
||||
dst.Elem().Set(src)
|
||||
} else if dst.CanSet() {
|
||||
dst.Set(src)
|
||||
}
|
||||
} else {
|
||||
e := src.Type().Elem()
|
||||
if dst.CanSet() && !src.IsNil() {
|
||||
if _, ok := src.Interface().(*time.Time); !ok {
|
||||
dst.Set(reflect.New(e))
|
||||
} else {
|
||||
tempValue := reflect.New(e)
|
||||
tempValue.Elem().Set(src.Elem())
|
||||
// Sets time.Time's unexported values
|
||||
dst.Set(tempValue)
|
||||
}
|
||||
}
|
||||
if src.Elem().IsValid() {
|
||||
// Keep the current root state since the depth hasn't changed
|
||||
rcopy(dst.Elem(), src.Elem(), root)
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
t := dst.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name := t.Field(i).Name
|
||||
srcVal := src.FieldByName(name)
|
||||
dstVal := dst.FieldByName(name)
|
||||
if srcVal.IsValid() && dstVal.CanSet() {
|
||||
rcopy(dstVal, srcVal, false)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeSlice(src.Type(), src.Len(), src.Cap())
|
||||
dst.Set(s)
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
rcopy(dst.Index(i), src.Index(i), false)
|
||||
}
|
||||
case reflect.Map:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeMap(src.Type())
|
||||
dst.Set(s)
|
||||
for _, k := range src.MapKeys() {
|
||||
v := src.MapIndex(k)
|
||||
v2 := reflect.New(v.Type()).Elem()
|
||||
rcopy(v2, v, false)
|
||||
dst.SetMapIndex(k, v2)
|
||||
}
|
||||
default:
|
||||
// Assign the value if possible. If its not assignable, the value would
|
||||
// need to be converted and the impact of that may be unexpected, or is
|
||||
// not compatible with the dst type.
|
||||
if src.Type().AssignableTo(dst.Type()) {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
vendor/github.com/aws/aws-sdk-go/aws/awsutil/equal.go
generated
vendored
Normal file
27
vendor/github.com/aws/aws-sdk-go/aws/awsutil/equal.go
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// DeepEqual returns if the two values are deeply equal like reflect.DeepEqual.
|
||||
// In addition to this, this method will also dereference the input values if
|
||||
// possible so the DeepEqual performed will not fail if one parameter is a
|
||||
// pointer and the other is not.
|
||||
//
|
||||
// DeepEqual will not perform indirection of nested values of the input parameters.
|
||||
func DeepEqual(a, b interface{}) bool {
|
||||
ra := reflect.Indirect(reflect.ValueOf(a))
|
||||
rb := reflect.Indirect(reflect.ValueOf(b))
|
||||
|
||||
if raValid, rbValid := ra.IsValid(), rb.IsValid(); !raValid && !rbValid {
|
||||
// If the elements are both nil, and of the same type the are equal
|
||||
// If they are of different types they are not equal
|
||||
return reflect.TypeOf(a) == reflect.TypeOf(b)
|
||||
} else if raValid != rbValid {
|
||||
// Both values must be valid to be equal
|
||||
return false
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(ra.Interface(), rb.Interface())
|
||||
}
|
||||
222
vendor/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go
generated
vendored
Normal file
222
vendor/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go
generated
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmespath/go-jmespath"
|
||||
)
|
||||
|
||||
var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
|
||||
|
||||
// rValuesAtPath returns a slice of values found in value v. The values
|
||||
// in v are explored recursively so all nested values are collected.
|
||||
func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
|
||||
pathparts := strings.Split(path, "||")
|
||||
if len(pathparts) > 1 {
|
||||
for _, pathpart := range pathparts {
|
||||
vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
|
||||
if len(vals) > 0 {
|
||||
return vals
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
values := []reflect.Value{reflect.Indirect(reflect.ValueOf(v))}
|
||||
components := strings.Split(path, ".")
|
||||
for len(values) > 0 && len(components) > 0 {
|
||||
var index *int64
|
||||
var indexStar bool
|
||||
c := strings.TrimSpace(components[0])
|
||||
if c == "" { // no actual component, illegal syntax
|
||||
return nil
|
||||
} else if caseSensitive && c != "*" && strings.ToLower(c[0:1]) == c[0:1] {
|
||||
// TODO normalize case for user
|
||||
return nil // don't support unexported fields
|
||||
}
|
||||
|
||||
// parse this component
|
||||
if m := indexRe.FindStringSubmatch(c); m != nil {
|
||||
c = m[1]
|
||||
if m[2] == "" {
|
||||
index = nil
|
||||
indexStar = true
|
||||
} else {
|
||||
i, _ := strconv.ParseInt(m[2], 10, 32)
|
||||
index = &i
|
||||
indexStar = false
|
||||
}
|
||||
}
|
||||
|
||||
nextvals := []reflect.Value{}
|
||||
for _, value := range values {
|
||||
// pull component name out of struct member
|
||||
if value.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
|
||||
if c == "*" { // pull all members
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
if f := reflect.Indirect(value.Field(i)); f.IsValid() {
|
||||
nextvals = append(nextvals, f)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
value = value.FieldByNameFunc(func(name string) bool {
|
||||
if c == name {
|
||||
return true
|
||||
} else if !caseSensitive && strings.ToLower(name) == strings.ToLower(c) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
|
||||
if !value.IsNil() {
|
||||
value.Set(reflect.Zero(value.Type()))
|
||||
}
|
||||
return []reflect.Value{value}
|
||||
}
|
||||
|
||||
if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
|
||||
// TODO if the value is the terminus it should not be created
|
||||
// if the value to be set to its position is nil.
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
value = value.Elem()
|
||||
} else {
|
||||
value = reflect.Indirect(value)
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
|
||||
if indexStar || index != nil {
|
||||
nextvals = []reflect.Value{}
|
||||
for _, valItem := range values {
|
||||
value := reflect.Indirect(valItem)
|
||||
if value.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
if indexStar { // grab all indices
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
idx := reflect.Indirect(value.Index(i))
|
||||
if idx.IsValid() {
|
||||
nextvals = append(nextvals, idx)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// pull out index
|
||||
i := int(*index)
|
||||
if i >= value.Len() { // check out of bounds
|
||||
if createPath {
|
||||
// TODO resize slice
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if i < 0 { // support negative indexing
|
||||
i = value.Len() + i
|
||||
}
|
||||
value = reflect.Indirect(value.Index(i))
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
}
|
||||
|
||||
components = components[1:]
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// ValuesAtPath returns a list of values at the case insensitive lexical
|
||||
// path inside of a structure.
|
||||
func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
|
||||
result, err := jmespath.Search(path, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(result)
|
||||
if !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) {
|
||||
return nil, nil
|
||||
}
|
||||
if s, ok := result.([]interface{}); ok {
|
||||
return s, err
|
||||
}
|
||||
if v.Kind() == reflect.Map && v.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if v.Kind() == reflect.Slice {
|
||||
out := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
out[i] = v.Index(i).Interface()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return []interface{}{result}, nil
|
||||
}
|
||||
|
||||
// SetValueAtPath sets a value at the case insensitive lexical path inside
|
||||
// of a structure.
|
||||
func SetValueAtPath(i interface{}, path string, v interface{}) {
|
||||
if rvals := rValuesAtPath(i, path, true, false, v == nil); rvals != nil {
|
||||
for _, rval := range rvals {
|
||||
if rval.Kind() == reflect.Ptr && rval.IsNil() {
|
||||
continue
|
||||
}
|
||||
setValue(rval, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(dstVal reflect.Value, src interface{}) {
|
||||
if dstVal.Kind() == reflect.Ptr {
|
||||
dstVal = reflect.Indirect(dstVal)
|
||||
}
|
||||
srcVal := reflect.ValueOf(src)
|
||||
|
||||
if !srcVal.IsValid() { // src is literal nil
|
||||
if dstVal.CanAddr() {
|
||||
// Convert to pointer so that pointer's value can be nil'ed
|
||||
// dstVal = dstVal.Addr()
|
||||
}
|
||||
dstVal.Set(reflect.Zero(dstVal.Type()))
|
||||
|
||||
} else if srcVal.Kind() == reflect.Ptr {
|
||||
if srcVal.IsNil() {
|
||||
srcVal = reflect.Zero(dstVal.Type())
|
||||
} else {
|
||||
srcVal = reflect.ValueOf(src).Elem()
|
||||
}
|
||||
dstVal.Set(srcVal)
|
||||
} else {
|
||||
dstVal.Set(srcVal)
|
||||
}
|
||||
|
||||
}
|
||||
113
vendor/github.com/aws/aws-sdk-go/aws/awsutil/prettify.go
generated
vendored
Normal file
113
vendor/github.com/aws/aws-sdk-go/aws/awsutil/prettify.go
generated
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Prettify returns the string representation of a value.
|
||||
func Prettify(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
prettify(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// prettify will recursively walk value v to build a textual
|
||||
// representation of the value.
|
||||
func prettify(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "time.Time" {
|
||||
fmt.Fprintf(buf, "%s", v.Interface())
|
||||
break
|
||||
} else if strings.HasPrefix(strtype, "io.") {
|
||||
buf.WriteString("<buffer>")
|
||||
break
|
||||
}
|
||||
|
||||
buf.WriteString("{\n")
|
||||
|
||||
names := []string{}
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
name := v.Type().Field(i).Name
|
||||
f := v.Field(i)
|
||||
if name[0:1] == strings.ToLower(name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice || f.Kind() == reflect.Map) && f.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
for i, n := range names {
|
||||
val := v.FieldByName(n)
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(n + ": ")
|
||||
prettify(val, indent+2, buf)
|
||||
|
||||
if i < len(names)-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "[]uint8" {
|
||||
fmt.Fprintf(buf, "<binary> len %d", v.Len())
|
||||
break
|
||||
}
|
||||
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
prettify(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
prettify(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
if !v.IsValid() {
|
||||
fmt.Fprint(buf, "<invalid value>")
|
||||
return
|
||||
}
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
case io.ReadSeeker, io.Reader:
|
||||
format = "buffer(%p)"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
89
vendor/github.com/aws/aws-sdk-go/aws/awsutil/string_value.go
generated
vendored
Normal file
89
vendor/github.com/aws/aws-sdk-go/aws/awsutil/string_value.go
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringValue returns the string representation of a value.
|
||||
func StringValue(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
stringValue(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func stringValue(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
names := []string{}
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
name := v.Type().Field(i).Name
|
||||
f := v.Field(i)
|
||||
if name[0:1] == strings.ToLower(name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice) && f.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
for i, n := range names {
|
||||
val := v.FieldByName(n)
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(n + ": ")
|
||||
stringValue(val, indent+2, buf)
|
||||
|
||||
if i < len(names)-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
stringValue(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
stringValue(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
96
vendor/github.com/aws/aws-sdk-go/aws/client/client.go
generated
vendored
Normal file
96
vendor/github.com/aws/aws-sdk-go/aws/client/client.go
generated
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client/metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
// A Config provides configuration to a service client instance.
|
||||
type Config struct {
|
||||
Config *aws.Config
|
||||
Handlers request.Handlers
|
||||
Endpoint string
|
||||
SigningRegion string
|
||||
SigningName string
|
||||
|
||||
// States that the signing name did not come from a modeled source but
|
||||
// was derived based on other data. Used by service client constructors
|
||||
// to determine if the signin name can be overriden based on metadata the
|
||||
// service has.
|
||||
SigningNameDerived bool
|
||||
}
|
||||
|
||||
// ConfigProvider provides a generic way for a service client to receive
|
||||
// the ClientConfig without circular dependencies.
|
||||
type ConfigProvider interface {
|
||||
ClientConfig(serviceName string, cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// ConfigNoResolveEndpointProvider same as ConfigProvider except it will not
|
||||
// resolve the endpoint automatically. The service client's endpoint must be
|
||||
// provided via the aws.Config.Endpoint field.
|
||||
type ConfigNoResolveEndpointProvider interface {
|
||||
ClientConfigNoResolveEndpoint(cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// A Client implements the base client request and response handling
|
||||
// used by all service clients.
|
||||
type Client struct {
|
||||
request.Retryer
|
||||
metadata.ClientInfo
|
||||
|
||||
Config aws.Config
|
||||
Handlers request.Handlers
|
||||
}
|
||||
|
||||
// New will return a pointer to a new initialized service client.
|
||||
func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, options ...func(*Client)) *Client {
|
||||
svc := &Client{
|
||||
Config: cfg,
|
||||
ClientInfo: info,
|
||||
Handlers: handlers.Copy(),
|
||||
}
|
||||
|
||||
switch retryer, ok := cfg.Retryer.(request.Retryer); {
|
||||
case ok:
|
||||
svc.Retryer = retryer
|
||||
case cfg.Retryer != nil && cfg.Logger != nil:
|
||||
s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
|
||||
cfg.Logger.Log(s)
|
||||
fallthrough
|
||||
default:
|
||||
maxRetries := aws.IntValue(cfg.MaxRetries)
|
||||
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
|
||||
maxRetries = 3
|
||||
}
|
||||
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
|
||||
}
|
||||
|
||||
svc.AddDebugHandlers()
|
||||
|
||||
for _, option := range options {
|
||||
option(svc)
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// NewRequest returns a new Request pointer for the service API
|
||||
// operation and parameters.
|
||||
func (c *Client) NewRequest(operation *request.Operation, params interface{}, data interface{}) *request.Request {
|
||||
return request.New(c.Config, c.ClientInfo, c.Handlers, c.Retryer, operation, params, data)
|
||||
}
|
||||
|
||||
// AddDebugHandlers injects debug logging handlers into the service to log request
|
||||
// debug information.
|
||||
func (c *Client) AddDebugHandlers() {
|
||||
if !c.Config.LogLevel.AtLeast(aws.LogDebug) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
|
||||
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
|
||||
}
|
||||
116
vendor/github.com/aws/aws-sdk-go/aws/client/default_retryer.go
generated
vendored
Normal file
116
vendor/github.com/aws/aws-sdk-go/aws/client/default_retryer.go
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/internal/sdkrand"
|
||||
)
|
||||
|
||||
// DefaultRetryer implements basic retry logic using exponential backoff for
|
||||
// most services. If you want to implement custom retry logic, implement the
|
||||
// request.Retryer interface or create a structure type that composes this
|
||||
// struct and override the specific methods. For example, to override only
|
||||
// the MaxRetries method:
|
||||
//
|
||||
// type retryer struct {
|
||||
// client.DefaultRetryer
|
||||
// }
|
||||
//
|
||||
// // This implementation always has 100 max retries
|
||||
// func (d retryer) MaxRetries() int { return 100 }
|
||||
type DefaultRetryer struct {
|
||||
NumMaxRetries int
|
||||
}
|
||||
|
||||
// MaxRetries returns the number of maximum returns the service will use to make
|
||||
// an individual API request.
|
||||
func (d DefaultRetryer) MaxRetries() int {
|
||||
return d.NumMaxRetries
|
||||
}
|
||||
|
||||
// RetryRules returns the delay duration before retrying this request again
|
||||
func (d DefaultRetryer) RetryRules(r *request.Request) time.Duration {
|
||||
// Set the upper limit of delay in retrying at ~five minutes
|
||||
minTime := 30
|
||||
throttle := d.shouldThrottle(r)
|
||||
if throttle {
|
||||
if delay, ok := getRetryDelay(r); ok {
|
||||
return delay
|
||||
}
|
||||
|
||||
minTime = 500
|
||||
}
|
||||
|
||||
retryCount := r.RetryCount
|
||||
if throttle && retryCount > 8 {
|
||||
retryCount = 8
|
||||
} else if retryCount > 13 {
|
||||
retryCount = 13
|
||||
}
|
||||
|
||||
delay := (1 << uint(retryCount)) * (sdkrand.SeededRand.Intn(minTime) + minTime)
|
||||
return time.Duration(delay) * time.Millisecond
|
||||
}
|
||||
|
||||
// ShouldRetry returns true if the request should be retried.
|
||||
func (d DefaultRetryer) ShouldRetry(r *request.Request) bool {
|
||||
// If one of the other handlers already set the retry state
|
||||
// we don't want to override it based on the service's state
|
||||
if r.Retryable != nil {
|
||||
return *r.Retryable
|
||||
}
|
||||
|
||||
if r.HTTPResponse.StatusCode >= 500 && r.HTTPResponse.StatusCode != 501 {
|
||||
return true
|
||||
}
|
||||
return r.IsErrorRetryable() || d.shouldThrottle(r)
|
||||
}
|
||||
|
||||
// ShouldThrottle returns true if the request should be throttled.
|
||||
func (d DefaultRetryer) shouldThrottle(r *request.Request) bool {
|
||||
switch r.HTTPResponse.StatusCode {
|
||||
case 429:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
default:
|
||||
return r.IsErrorThrottle()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// This will look in the Retry-After header, RFC 7231, for how long
|
||||
// it will wait before attempting another request
|
||||
func getRetryDelay(r *request.Request) (time.Duration, bool) {
|
||||
if !canUseRetryAfterHeader(r) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delayStr := r.HTTPResponse.Header.Get("Retry-After")
|
||||
if len(delayStr) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delay, err := strconv.Atoi(delayStr)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return time.Duration(delay) * time.Second, true
|
||||
}
|
||||
|
||||
// Will look at the status code to see if the retry header pertains to
|
||||
// the status code.
|
||||
func canUseRetryAfterHeader(r *request.Request) bool {
|
||||
switch r.HTTPResponse.StatusCode {
|
||||
case 429:
|
||||
case 503:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
184
vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
generated
vendored
Normal file
184
vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
generated
vendored
Normal file
@@ -0,0 +1,184 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
const logReqMsg = `DEBUG: Request %s/%s Details:
|
||||
---[ REQUEST POST-SIGN ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logReqErrMsg = `DEBUG ERROR: Request %s/%s:
|
||||
---[ REQUEST DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
------------------------------------------------------`
|
||||
|
||||
type logWriter struct {
|
||||
// Logger is what we will use to log the payload of a response.
|
||||
Logger aws.Logger
|
||||
// buf stores the contents of what has been read
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (logger *logWriter) Write(b []byte) (int, error) {
|
||||
return logger.buf.Write(b)
|
||||
}
|
||||
|
||||
type teeReaderCloser struct {
|
||||
// io.Reader will be a tee reader that is used during logging.
|
||||
// This structure will read from a body and write the contents to a logger.
|
||||
io.Reader
|
||||
// Source is used just to close when we are done reading.
|
||||
Source io.ReadCloser
|
||||
}
|
||||
|
||||
func (reader *teeReaderCloser) Close() error {
|
||||
return reader.Source.Close()
|
||||
}
|
||||
|
||||
// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will include the HTTP request body if the LogLevel of the
|
||||
// request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPRequestHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequest",
|
||||
Fn: logRequest,
|
||||
}
|
||||
|
||||
func logRequest(r *request.Request) {
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
bodySeekable := aws.IsReaderSeekable(r.Body)
|
||||
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
if logBody {
|
||||
if !bodySeekable {
|
||||
r.SetReaderBody(aws.ReadSeekCloser(r.HTTPRequest.Body))
|
||||
}
|
||||
// Reset the request body because dumpRequest will re-wrap the r.HTTPRequest's
|
||||
// Body as a NoOpCloser and will not be reset after read by the HTTP
|
||||
// client reader.
|
||||
r.ResetBody()
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will only log the HTTP request's headers. The request payload
|
||||
// will not be read.
|
||||
var LogHTTPRequestHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequestHeader",
|
||||
Fn: logRequestHeader,
|
||||
}
|
||||
|
||||
func logRequestHeader(r *request.Request) {
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
const logRespMsg = `DEBUG: Response %s/%s Details:
|
||||
---[ RESPONSE ]--------------------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
|
||||
---[ RESPONSE DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
|
||||
// received from a service. Will include the HTTP response body if the LogLevel
|
||||
// of the request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPResponseHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponse",
|
||||
Fn: logResponse,
|
||||
}
|
||||
|
||||
func logResponse(r *request.Request) {
|
||||
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
|
||||
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
if logBody {
|
||||
r.HTTPResponse.Body = &teeReaderCloser{
|
||||
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
|
||||
Source: r.HTTPResponse.Body,
|
||||
}
|
||||
}
|
||||
|
||||
handlerFn := func(req *request.Request) {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, false)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
|
||||
|
||||
if logBody {
|
||||
b, err := ioutil.ReadAll(lw.buf)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
const handlerName = "awsdk.client.LogResponse.ResponseBody"
|
||||
|
||||
r.Handlers.Unmarshal.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
r.Handlers.UnmarshalError.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
|
||||
// response received from a service. Will only log the HTTP response's headers.
|
||||
// The response payload will not be read.
|
||||
var LogHTTPResponseHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponseHeader",
|
||||
Fn: logResponseHeader,
|
||||
}
|
||||
|
||||
func logResponseHeader(r *request.Request) {
|
||||
if r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := httputil.DumpResponse(r.HTTPResponse, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
13
vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
generated
vendored
Normal file
13
vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package metadata
|
||||
|
||||
// ClientInfo wraps immutable data from the client.Client structure.
|
||||
type ClientInfo struct {
|
||||
ServiceName string
|
||||
ServiceID string
|
||||
APIVersion string
|
||||
Endpoint string
|
||||
SigningName string
|
||||
SigningRegion string
|
||||
JSONVersion string
|
||||
TargetPrefix string
|
||||
}
|
||||
492
vendor/github.com/aws/aws-sdk-go/aws/config.go
generated
vendored
Normal file
492
vendor/github.com/aws/aws-sdk-go/aws/config.go
generated
vendored
Normal file
@@ -0,0 +1,492 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
)
|
||||
|
||||
// UseServiceDefaultRetries instructs the config to use the service's own
|
||||
// default number of retries. This will be the default action if
|
||||
// Config.MaxRetries is nil also.
|
||||
const UseServiceDefaultRetries = -1
|
||||
|
||||
// RequestRetryer is an alias for a type that implements the request.Retryer
|
||||
// interface.
|
||||
type RequestRetryer interface{}
|
||||
|
||||
// A Config provides service configuration for service clients. By default,
|
||||
// all clients will use the defaults.DefaultConfig tructure.
|
||||
//
|
||||
// // Create Session with MaxRetry configuration to be shared by multiple
|
||||
// // service clients.
|
||||
// sess := session.Must(session.NewSession(&aws.Config{
|
||||
// MaxRetries: aws.Int(3),
|
||||
// }))
|
||||
//
|
||||
// // Create S3 service client with a specific Region.
|
||||
// svc := s3.New(sess, &aws.Config{
|
||||
// Region: aws.String("us-west-2"),
|
||||
// })
|
||||
type Config struct {
|
||||
// Enables verbose error printing of all credential chain errors.
|
||||
// Should be used when wanting to see all errors while attempting to
|
||||
// retrieve credentials.
|
||||
CredentialsChainVerboseErrors *bool
|
||||
|
||||
// The credentials object to use when signing requests. Defaults to a
|
||||
// chain of credential providers to search for credentials in environment
|
||||
// variables, shared credential file, and EC2 Instance Roles.
|
||||
Credentials *credentials.Credentials
|
||||
|
||||
// An optional endpoint URL (hostname only or fully qualified URI)
|
||||
// that overrides the default generated endpoint for a client. Set this
|
||||
// to `""` to use the default generated endpoint.
|
||||
//
|
||||
// @note You must still provide a `Region` value when specifying an
|
||||
// endpoint for a client.
|
||||
Endpoint *string
|
||||
|
||||
// The resolver to use for looking up endpoints for AWS service clients
|
||||
// to use based on region.
|
||||
EndpointResolver endpoints.Resolver
|
||||
|
||||
// EnforceShouldRetryCheck is used in the AfterRetryHandler to always call
|
||||
// ShouldRetry regardless of whether or not if request.Retryable is set.
|
||||
// This will utilize ShouldRetry method of custom retryers. If EnforceShouldRetryCheck
|
||||
// is not set, then ShouldRetry will only be called if request.Retryable is nil.
|
||||
// Proper handling of the request.Retryable field is important when setting this field.
|
||||
EnforceShouldRetryCheck *bool
|
||||
|
||||
// The region to send requests to. This parameter is required and must
|
||||
// be configured globally or on a per-client basis unless otherwise
|
||||
// noted. A full list of regions is found in the "Regions and Endpoints"
|
||||
// document.
|
||||
//
|
||||
// @see http://docs.aws.amazon.com/general/latest/gr/rande.html
|
||||
// AWS Regions and Endpoints
|
||||
Region *string
|
||||
|
||||
// Set this to `true` to disable SSL when sending requests. Defaults
|
||||
// to `false`.
|
||||
DisableSSL *bool
|
||||
|
||||
// The HTTP client to use when sending requests. Defaults to
|
||||
// `http.DefaultClient`.
|
||||
HTTPClient *http.Client
|
||||
|
||||
// An integer value representing the logging level. The default log level
|
||||
// is zero (LogOff), which represents no logging. To enable logging set
|
||||
// to a LogLevel Value.
|
||||
LogLevel *LogLevelType
|
||||
|
||||
// The logger writer interface to write logging messages to. Defaults to
|
||||
// standard out.
|
||||
Logger Logger
|
||||
|
||||
// The maximum number of times that a request will be retried for failures.
|
||||
// Defaults to -1, which defers the max retry setting to the service
|
||||
// specific configuration.
|
||||
MaxRetries *int
|
||||
|
||||
// Retryer guides how HTTP requests should be retried in case of
|
||||
// recoverable failures.
|
||||
//
|
||||
// When nil or the value does not implement the request.Retryer interface,
|
||||
// the client.DefaultRetryer will be used.
|
||||
//
|
||||
// When both Retryer and MaxRetries are non-nil, the former is used and
|
||||
// the latter ignored.
|
||||
//
|
||||
// To set the Retryer field in a type-safe manner and with chaining, use
|
||||
// the request.WithRetryer helper function:
|
||||
//
|
||||
// cfg := request.WithRetryer(aws.NewConfig(), myRetryer)
|
||||
//
|
||||
Retryer RequestRetryer
|
||||
|
||||
// Disables semantic parameter validation, which validates input for
|
||||
// missing required fields and/or other semantic request input errors.
|
||||
DisableParamValidation *bool
|
||||
|
||||
// Disables the computation of request and response checksums, e.g.,
|
||||
// CRC32 checksums in Amazon DynamoDB.
|
||||
DisableComputeChecksums *bool
|
||||
|
||||
// Set this to `true` to force the request to use path-style addressing,
|
||||
// i.e., `http://s3.amazonaws.com/BUCKET/KEY`. By default, the S3 client
|
||||
// will use virtual hosted bucket addressing when possible
|
||||
// (`http://BUCKET.s3.amazonaws.com/KEY`).
|
||||
//
|
||||
// @note This configuration option is specific to the Amazon S3 service.
|
||||
// @see http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
|
||||
// Amazon S3: Virtual Hosting of Buckets
|
||||
S3ForcePathStyle *bool
|
||||
|
||||
// Set this to `true` to disable the SDK adding the `Expect: 100-Continue`
|
||||
// header to PUT requests over 2MB of content. 100-Continue instructs the
|
||||
// HTTP client not to send the body until the service responds with a
|
||||
// `continue` status. This is useful to prevent sending the request body
|
||||
// until after the request is authenticated, and validated.
|
||||
//
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html
|
||||
//
|
||||
// 100-Continue is only enabled for Go 1.6 and above. See `http.Transport`'s
|
||||
// `ExpectContinueTimeout` for information on adjusting the continue wait
|
||||
// timeout. https://golang.org/pkg/net/http/#Transport
|
||||
//
|
||||
// You should use this flag to disble 100-Continue if you experience issues
|
||||
// with proxies or third party S3 compatible services.
|
||||
S3Disable100Continue *bool
|
||||
|
||||
// Set this to `true` to enable S3 Accelerate feature. For all operations
|
||||
// compatible with S3 Accelerate will use the accelerate endpoint for
|
||||
// requests. Requests not compatible will fall back to normal S3 requests.
|
||||
//
|
||||
// The bucket must be enable for accelerate to be used with S3 client with
|
||||
// accelerate enabled. If the bucket is not enabled for accelerate an error
|
||||
// will be returned. The bucket name must be DNS compatible to also work
|
||||
// with accelerate.
|
||||
S3UseAccelerate *bool
|
||||
|
||||
// S3DisableContentMD5Validation config option is temporarily disabled,
|
||||
// For S3 GetObject API calls, #1837.
|
||||
//
|
||||
// Set this to `true` to disable the S3 service client from automatically
|
||||
// adding the ContentMD5 to S3 Object Put and Upload API calls. This option
|
||||
// will also disable the SDK from performing object ContentMD5 validation
|
||||
// on GetObject API calls.
|
||||
S3DisableContentMD5Validation *bool
|
||||
|
||||
// Set this to `true` to disable the EC2Metadata client from overriding the
|
||||
// default http.Client's Timeout. This is helpful if you do not want the
|
||||
// EC2Metadata client to create a new http.Client. This options is only
|
||||
// meaningful if you're not already using a custom HTTP client with the
|
||||
// SDK. Enabled by default.
|
||||
//
|
||||
// Must be set and provided to the session.NewSession() in order to disable
|
||||
// the EC2Metadata overriding the timeout for default credentials chain.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(aws.NewConfig()
|
||||
// .WithEC2MetadataDiableTimeoutOverride(true)))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
//
|
||||
EC2MetadataDisableTimeoutOverride *bool
|
||||
|
||||
// Instructs the endpoint to be generated for a service client to
|
||||
// be the dual stack endpoint. The dual stack endpoint will support
|
||||
// both IPv4 and IPv6 addressing.
|
||||
//
|
||||
// Setting this for a service which does not support dual stack will fail
|
||||
// to make requets. It is not recommended to set this value on the session
|
||||
// as it will apply to all service clients created with the session. Even
|
||||
// services which don't support dual stack endpoints.
|
||||
//
|
||||
// If the Endpoint config value is also provided the UseDualStack flag
|
||||
// will be ignored.
|
||||
//
|
||||
// Only supported with.
|
||||
//
|
||||
// sess := session.Must(session.NewSession())
|
||||
//
|
||||
// svc := s3.New(sess, &aws.Config{
|
||||
// UseDualStack: aws.Bool(true),
|
||||
// })
|
||||
UseDualStack *bool
|
||||
|
||||
// SleepDelay is an override for the func the SDK will call when sleeping
|
||||
// during the lifecycle of a request. Specifically this will be used for
|
||||
// request delays. This value should only be used for testing. To adjust
|
||||
// the delay of a request see the aws/client.DefaultRetryer and
|
||||
// aws/request.Retryer.
|
||||
//
|
||||
// SleepDelay will prevent any Context from being used for canceling retry
|
||||
// delay of an API operation. It is recommended to not use SleepDelay at all
|
||||
// and specify a Retryer instead.
|
||||
SleepDelay func(time.Duration)
|
||||
|
||||
// DisableRestProtocolURICleaning will not clean the URL path when making rest protocol requests.
|
||||
// Will default to false. This would only be used for empty directory names in s3 requests.
|
||||
//
|
||||
// Example:
|
||||
// sess := session.Must(session.NewSession(&aws.Config{
|
||||
// DisableRestProtocolURICleaning: aws.Bool(true),
|
||||
// }))
|
||||
//
|
||||
// svc := s3.New(sess)
|
||||
// out, err := svc.GetObject(&s3.GetObjectInput {
|
||||
// Bucket: aws.String("bucketname"),
|
||||
// Key: aws.String("//foo//bar//moo"),
|
||||
// })
|
||||
DisableRestProtocolURICleaning *bool
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config pointer that can be chained with builder
|
||||
// methods to set multiple configuration values inline without using pointers.
|
||||
//
|
||||
// // Create Session with MaxRetry configuration to be shared by multiple
|
||||
// // service clients.
|
||||
// sess := session.Must(session.NewSession(aws.NewConfig().
|
||||
// WithMaxRetries(3),
|
||||
// ))
|
||||
//
|
||||
// // Create S3 service client with a specific Region.
|
||||
// svc := s3.New(sess, aws.NewConfig().
|
||||
// WithRegion("us-west-2"),
|
||||
// )
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// WithCredentialsChainVerboseErrors sets a config verbose errors boolean and returning
|
||||
// a Config pointer.
|
||||
func (c *Config) WithCredentialsChainVerboseErrors(verboseErrs bool) *Config {
|
||||
c.CredentialsChainVerboseErrors = &verboseErrs
|
||||
return c
|
||||
}
|
||||
|
||||
// WithCredentials sets a config Credentials value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithCredentials(creds *credentials.Credentials) *Config {
|
||||
c.Credentials = creds
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEndpoint sets a config Endpoint value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithEndpoint(endpoint string) *Config {
|
||||
c.Endpoint = &endpoint
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEndpointResolver sets a config EndpointResolver value returning a
|
||||
// Config pointer for chaining.
|
||||
func (c *Config) WithEndpointResolver(resolver endpoints.Resolver) *Config {
|
||||
c.EndpointResolver = resolver
|
||||
return c
|
||||
}
|
||||
|
||||
// WithRegion sets a config Region value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithRegion(region string) *Config {
|
||||
c.Region = ®ion
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableSSL sets a config DisableSSL value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithDisableSSL(disable bool) *Config {
|
||||
c.DisableSSL = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithHTTPClient sets a config HTTPClient value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithHTTPClient(client *http.Client) *Config {
|
||||
c.HTTPClient = client
|
||||
return c
|
||||
}
|
||||
|
||||
// WithMaxRetries sets a config MaxRetries value returning a Config pointer
|
||||
// for chaining.
|
||||
func (c *Config) WithMaxRetries(max int) *Config {
|
||||
c.MaxRetries = &max
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableParamValidation sets a config DisableParamValidation value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithDisableParamValidation(disable bool) *Config {
|
||||
c.DisableParamValidation = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithDisableComputeChecksums sets a config DisableComputeChecksums value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithDisableComputeChecksums(disable bool) *Config {
|
||||
c.DisableComputeChecksums = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLogLevel sets a config LogLevel value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithLogLevel(level LogLevelType) *Config {
|
||||
c.LogLevel = &level
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLogger sets a config Logger value returning a Config pointer for
|
||||
// chaining.
|
||||
func (c *Config) WithLogger(logger Logger) *Config {
|
||||
c.Logger = logger
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3ForcePathStyle sets a config S3ForcePathStyle value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithS3ForcePathStyle(force bool) *Config {
|
||||
c.S3ForcePathStyle = &force
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3Disable100Continue sets a config S3Disable100Continue value returning
|
||||
// a Config pointer for chaining.
|
||||
func (c *Config) WithS3Disable100Continue(disable bool) *Config {
|
||||
c.S3Disable100Continue = &disable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithS3UseAccelerate sets a config S3UseAccelerate value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithS3UseAccelerate(enable bool) *Config {
|
||||
c.S3UseAccelerate = &enable
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
// WithS3DisableContentMD5Validation sets a config
|
||||
// S3DisableContentMD5Validation value returning a Config pointer for chaining.
|
||||
func (c *Config) WithS3DisableContentMD5Validation(enable bool) *Config {
|
||||
c.S3DisableContentMD5Validation = &enable
|
||||
return c
|
||||
|
||||
}
|
||||
|
||||
// WithUseDualStack sets a config UseDualStack value returning a Config
|
||||
// pointer for chaining.
|
||||
func (c *Config) WithUseDualStack(enable bool) *Config {
|
||||
c.UseDualStack = &enable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithEC2MetadataDisableTimeoutOverride sets a config EC2MetadataDisableTimeoutOverride value
|
||||
// returning a Config pointer for chaining.
|
||||
func (c *Config) WithEC2MetadataDisableTimeoutOverride(enable bool) *Config {
|
||||
c.EC2MetadataDisableTimeoutOverride = &enable
|
||||
return c
|
||||
}
|
||||
|
||||
// WithSleepDelay overrides the function used to sleep while waiting for the
|
||||
// next retry. Defaults to time.Sleep.
|
||||
func (c *Config) WithSleepDelay(fn func(time.Duration)) *Config {
|
||||
c.SleepDelay = fn
|
||||
return c
|
||||
}
|
||||
|
||||
// MergeIn merges the passed in configs into the existing config object.
|
||||
func (c *Config) MergeIn(cfgs ...*Config) {
|
||||
for _, other := range cfgs {
|
||||
mergeInConfig(c, other)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeInConfig(dst *Config, other *Config) {
|
||||
if other == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if other.CredentialsChainVerboseErrors != nil {
|
||||
dst.CredentialsChainVerboseErrors = other.CredentialsChainVerboseErrors
|
||||
}
|
||||
|
||||
if other.Credentials != nil {
|
||||
dst.Credentials = other.Credentials
|
||||
}
|
||||
|
||||
if other.Endpoint != nil {
|
||||
dst.Endpoint = other.Endpoint
|
||||
}
|
||||
|
||||
if other.EndpointResolver != nil {
|
||||
dst.EndpointResolver = other.EndpointResolver
|
||||
}
|
||||
|
||||
if other.Region != nil {
|
||||
dst.Region = other.Region
|
||||
}
|
||||
|
||||
if other.DisableSSL != nil {
|
||||
dst.DisableSSL = other.DisableSSL
|
||||
}
|
||||
|
||||
if other.HTTPClient != nil {
|
||||
dst.HTTPClient = other.HTTPClient
|
||||
}
|
||||
|
||||
if other.LogLevel != nil {
|
||||
dst.LogLevel = other.LogLevel
|
||||
}
|
||||
|
||||
if other.Logger != nil {
|
||||
dst.Logger = other.Logger
|
||||
}
|
||||
|
||||
if other.MaxRetries != nil {
|
||||
dst.MaxRetries = other.MaxRetries
|
||||
}
|
||||
|
||||
if other.Retryer != nil {
|
||||
dst.Retryer = other.Retryer
|
||||
}
|
||||
|
||||
if other.DisableParamValidation != nil {
|
||||
dst.DisableParamValidation = other.DisableParamValidation
|
||||
}
|
||||
|
||||
if other.DisableComputeChecksums != nil {
|
||||
dst.DisableComputeChecksums = other.DisableComputeChecksums
|
||||
}
|
||||
|
||||
if other.S3ForcePathStyle != nil {
|
||||
dst.S3ForcePathStyle = other.S3ForcePathStyle
|
||||
}
|
||||
|
||||
if other.S3Disable100Continue != nil {
|
||||
dst.S3Disable100Continue = other.S3Disable100Continue
|
||||
}
|
||||
|
||||
if other.S3UseAccelerate != nil {
|
||||
dst.S3UseAccelerate = other.S3UseAccelerate
|
||||
}
|
||||
|
||||
if other.S3DisableContentMD5Validation != nil {
|
||||
dst.S3DisableContentMD5Validation = other.S3DisableContentMD5Validation
|
||||
}
|
||||
|
||||
if other.UseDualStack != nil {
|
||||
dst.UseDualStack = other.UseDualStack
|
||||
}
|
||||
|
||||
if other.EC2MetadataDisableTimeoutOverride != nil {
|
||||
dst.EC2MetadataDisableTimeoutOverride = other.EC2MetadataDisableTimeoutOverride
|
||||
}
|
||||
|
||||
if other.SleepDelay != nil {
|
||||
dst.SleepDelay = other.SleepDelay
|
||||
}
|
||||
|
||||
if other.DisableRestProtocolURICleaning != nil {
|
||||
dst.DisableRestProtocolURICleaning = other.DisableRestProtocolURICleaning
|
||||
}
|
||||
|
||||
if other.EnforceShouldRetryCheck != nil {
|
||||
dst.EnforceShouldRetryCheck = other.EnforceShouldRetryCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Copy will return a shallow copy of the Config object. If any additional
|
||||
// configurations are provided they will be merged into the new config returned.
|
||||
func (c *Config) Copy(cfgs ...*Config) *Config {
|
||||
dst := &Config{}
|
||||
dst.MergeIn(c)
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
dst.MergeIn(cfg)
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
71
vendor/github.com/aws/aws-sdk-go/aws/context.go
generated
vendored
Normal file
71
vendor/github.com/aws/aws-sdk-go/aws/context.go
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context is an copy of the Go v1.7 stdlib's context.Context interface.
|
||||
// It is represented as a SDK interface to enable you to use the "WithContext"
|
||||
// API methods with Go v1.6 and a Context type such as golang.org/x/net/context.
|
||||
//
|
||||
// See https://golang.org/pkg/context on how to use contexts.
|
||||
type Context interface {
|
||||
// Deadline returns the time when work done on behalf of this context
|
||||
// should be canceled. Deadline returns ok==false when no deadline is
|
||||
// set. Successive calls to Deadline return the same results.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that's closed when work done on behalf of this
|
||||
// context should be canceled. Done may return nil if this context can
|
||||
// never be canceled. Successive calls to Done return the same value.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err returns a non-nil error value after Done is closed. Err returns
|
||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||
// context's deadline passed. No other values for Err are defined.
|
||||
// After Done is closed, successive calls to Err return the same value.
|
||||
Err() error
|
||||
|
||||
// Value returns the value associated with this context for key, or nil
|
||||
// if no value is associated with key. Successive calls to Value with
|
||||
// the same key returns the same result.
|
||||
//
|
||||
// Use context values only for request-scoped data that transits
|
||||
// processes and API boundaries, not for passing optional parameters to
|
||||
// functions.
|
||||
Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// BackgroundContext returns a context that will never be canceled, has no
|
||||
// values, and no deadline. This context is used by the SDK to provide
|
||||
// backwards compatibility with non-context API operations and functionality.
|
||||
//
|
||||
// Go 1.6 and before:
|
||||
// This context function is equivalent to context.Background in the Go stdlib.
|
||||
//
|
||||
// Go 1.7 and later:
|
||||
// The context returned will be the value returned by context.Background()
|
||||
//
|
||||
// See https://golang.org/pkg/context for more information on Contexts.
|
||||
func BackgroundContext() Context {
|
||||
return backgroundCtx
|
||||
}
|
||||
|
||||
// SleepWithContext will wait for the timer duration to expire, or the context
|
||||
// is canceled. Which ever happens first. If the context is canceled the Context's
|
||||
// error will be returned.
|
||||
//
|
||||
// Expects Context to always return a non-nil error if the Done channel is closed.
|
||||
func SleepWithContext(ctx Context, dur time.Duration) error {
|
||||
t := time.NewTimer(dur)
|
||||
defer t.Stop()
|
||||
|
||||
select {
|
||||
case <-t.C:
|
||||
break
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
41
vendor/github.com/aws/aws-sdk-go/aws/context_1_6.go
generated
vendored
Normal file
41
vendor/github.com/aws/aws-sdk-go/aws/context_1_6.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// +build !go1.7
|
||||
|
||||
package aws
|
||||
|
||||
import "time"
|
||||
|
||||
// An emptyCtx is a copy of the Go 1.7 context.emptyCtx type. This is copied to
|
||||
// provide a 1.6 and 1.5 safe version of context that is compatible with Go
|
||||
// 1.7's Context.
|
||||
//
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case backgroundCtx:
|
||||
return "aws.BackgroundContext"
|
||||
}
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
var (
|
||||
backgroundCtx = new(emptyCtx)
|
||||
)
|
||||
9
vendor/github.com/aws/aws-sdk-go/aws/context_1_7.go
generated
vendored
Normal file
9
vendor/github.com/aws/aws-sdk-go/aws/context_1_7.go
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build go1.7
|
||||
|
||||
package aws
|
||||
|
||||
import "context"
|
||||
|
||||
var (
|
||||
backgroundCtx = context.Background()
|
||||
)
|
||||
387
vendor/github.com/aws/aws-sdk-go/aws/convert_types.go
generated
vendored
Normal file
387
vendor/github.com/aws/aws-sdk-go/aws/convert_types.go
generated
vendored
Normal file
@@ -0,0 +1,387 @@
|
||||
package aws
|
||||
|
||||
import "time"
|
||||
|
||||
// String returns a pointer to the string value passed in.
|
||||
func String(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
// StringValue returns the value of the string pointer passed in or
|
||||
// "" if the pointer is nil.
|
||||
func StringValue(v *string) string {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// StringSlice converts a slice of string values into a slice of
|
||||
// string pointers
|
||||
func StringSlice(src []string) []*string {
|
||||
dst := make([]*string, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// StringValueSlice converts a slice of string pointers into a slice of
|
||||
// string values
|
||||
func StringValueSlice(src []*string) []string {
|
||||
dst := make([]string, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// StringMap converts a string map of string values into a string
|
||||
// map of string pointers
|
||||
func StringMap(src map[string]string) map[string]*string {
|
||||
dst := make(map[string]*string)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// StringValueMap converts a string map of string pointers into a string
|
||||
// map of string values
|
||||
func StringValueMap(src map[string]*string) map[string]string {
|
||||
dst := make(map[string]string)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Bool returns a pointer to the bool value passed in.
|
||||
func Bool(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
// BoolValue returns the value of the bool pointer passed in or
|
||||
// false if the pointer is nil.
|
||||
func BoolValue(v *bool) bool {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BoolSlice converts a slice of bool values into a slice of
|
||||
// bool pointers
|
||||
func BoolSlice(src []bool) []*bool {
|
||||
dst := make([]*bool, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// BoolValueSlice converts a slice of bool pointers into a slice of
|
||||
// bool values
|
||||
func BoolValueSlice(src []*bool) []bool {
|
||||
dst := make([]bool, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// BoolMap converts a string map of bool values into a string
|
||||
// map of bool pointers
|
||||
func BoolMap(src map[string]bool) map[string]*bool {
|
||||
dst := make(map[string]*bool)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// BoolValueMap converts a string map of bool pointers into a string
|
||||
// map of bool values
|
||||
func BoolValueMap(src map[string]*bool) map[string]bool {
|
||||
dst := make(map[string]bool)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int returns a pointer to the int value passed in.
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
// IntValue returns the value of the int pointer passed in or
|
||||
// 0 if the pointer is nil.
|
||||
func IntValue(v *int) int {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// IntSlice converts a slice of int values into a slice of
|
||||
// int pointers
|
||||
func IntSlice(src []int) []*int {
|
||||
dst := make([]*int, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// IntValueSlice converts a slice of int pointers into a slice of
|
||||
// int values
|
||||
func IntValueSlice(src []*int) []int {
|
||||
dst := make([]int, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// IntMap converts a string map of int values into a string
|
||||
// map of int pointers
|
||||
func IntMap(src map[string]int) map[string]*int {
|
||||
dst := make(map[string]*int)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// IntValueMap converts a string map of int pointers into a string
|
||||
// map of int values
|
||||
func IntValueMap(src map[string]*int) map[string]int {
|
||||
dst := make(map[string]int)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int64 returns a pointer to the int64 value passed in.
|
||||
func Int64(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Int64Value returns the value of the int64 pointer passed in or
|
||||
// 0 if the pointer is nil.
|
||||
func Int64Value(v *int64) int64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Int64Slice converts a slice of int64 values into a slice of
|
||||
// int64 pointers
|
||||
func Int64Slice(src []int64) []*int64 {
|
||||
dst := make([]*int64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int64ValueSlice converts a slice of int64 pointers into a slice of
|
||||
// int64 values
|
||||
func Int64ValueSlice(src []*int64) []int64 {
|
||||
dst := make([]int64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int64Map converts a string map of int64 values into a string
|
||||
// map of int64 pointers
|
||||
func Int64Map(src map[string]int64) map[string]*int64 {
|
||||
dst := make(map[string]*int64)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Int64ValueMap converts a string map of int64 pointers into a string
|
||||
// map of int64 values
|
||||
func Int64ValueMap(src map[string]*int64) map[string]int64 {
|
||||
dst := make(map[string]int64)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float64 returns a pointer to the float64 value passed in.
|
||||
func Float64(v float64) *float64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// Float64Value returns the value of the float64 pointer passed in or
|
||||
// 0 if the pointer is nil.
|
||||
func Float64Value(v *float64) float64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Float64Slice converts a slice of float64 values into a slice of
|
||||
// float64 pointers
|
||||
func Float64Slice(src []float64) []*float64 {
|
||||
dst := make([]*float64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float64ValueSlice converts a slice of float64 pointers into a slice of
|
||||
// float64 values
|
||||
func Float64ValueSlice(src []*float64) []float64 {
|
||||
dst := make([]float64, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float64Map converts a string map of float64 values into a string
|
||||
// map of float64 pointers
|
||||
func Float64Map(src map[string]float64) map[string]*float64 {
|
||||
dst := make(map[string]*float64)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Float64ValueMap converts a string map of float64 pointers into a string
|
||||
// map of float64 values
|
||||
func Float64ValueMap(src map[string]*float64) map[string]float64 {
|
||||
dst := make(map[string]float64)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Time returns a pointer to the time.Time value passed in.
|
||||
func Time(v time.Time) *time.Time {
|
||||
return &v
|
||||
}
|
||||
|
||||
// TimeValue returns the value of the time.Time pointer passed in or
|
||||
// time.Time{} if the pointer is nil.
|
||||
func TimeValue(v *time.Time) time.Time {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// SecondsTimeValue converts an int64 pointer to a time.Time value
|
||||
// representing seconds since Epoch or time.Time{} if the pointer is nil.
|
||||
func SecondsTimeValue(v *int64) time.Time {
|
||||
if v != nil {
|
||||
return time.Unix((*v / 1000), 0)
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// MillisecondsTimeValue converts an int64 pointer to a time.Time value
|
||||
// representing milliseconds sinch Epoch or time.Time{} if the pointer is nil.
|
||||
func MillisecondsTimeValue(v *int64) time.Time {
|
||||
if v != nil {
|
||||
return time.Unix(0, (*v * 1000000))
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// TimeUnixMilli returns a Unix timestamp in milliseconds from "January 1, 1970 UTC".
|
||||
// The result is undefined if the Unix time cannot be represented by an int64.
|
||||
// Which includes calling TimeUnixMilli on a zero Time is undefined.
|
||||
//
|
||||
// This utility is useful for service API's such as CloudWatch Logs which require
|
||||
// their unix time values to be in milliseconds.
|
||||
//
|
||||
// See Go stdlib https://golang.org/pkg/time/#Time.UnixNano for more information.
|
||||
func TimeUnixMilli(t time.Time) int64 {
|
||||
return t.UnixNano() / int64(time.Millisecond/time.Nanosecond)
|
||||
}
|
||||
|
||||
// TimeSlice converts a slice of time.Time values into a slice of
|
||||
// time.Time pointers
|
||||
func TimeSlice(src []time.Time) []*time.Time {
|
||||
dst := make([]*time.Time, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
dst[i] = &(src[i])
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// TimeValueSlice converts a slice of time.Time pointers into a slice of
|
||||
// time.Time values
|
||||
func TimeValueSlice(src []*time.Time) []time.Time {
|
||||
dst := make([]time.Time, len(src))
|
||||
for i := 0; i < len(src); i++ {
|
||||
if src[i] != nil {
|
||||
dst[i] = *(src[i])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// TimeMap converts a string map of time.Time values into a string
|
||||
// map of time.Time pointers
|
||||
func TimeMap(src map[string]time.Time) map[string]*time.Time {
|
||||
dst := make(map[string]*time.Time)
|
||||
for k, val := range src {
|
||||
v := val
|
||||
dst[k] = &v
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// TimeValueMap converts a string map of time.Time pointers into a string
|
||||
// map of time.Time values
|
||||
func TimeValueMap(src map[string]*time.Time) map[string]time.Time {
|
||||
dst := make(map[string]time.Time)
|
||||
for k, val := range src {
|
||||
if val != nil {
|
||||
dst[k] = *val
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
228
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/handlers.go
generated
vendored
Normal file
228
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/handlers.go
generated
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
package corehandlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
// Interface for matching types which also have a Len method.
|
||||
type lener interface {
|
||||
Len() int
|
||||
}
|
||||
|
||||
// BuildContentLengthHandler builds the content length of a request based on the body,
|
||||
// or will use the HTTPRequest.Header's "Content-Length" if defined. If unable
|
||||
// to determine request body length and no "Content-Length" was specified it will panic.
|
||||
//
|
||||
// The Content-Length will only be added to the request if the length of the body
|
||||
// is greater than 0. If the body is empty or the current `Content-Length`
|
||||
// header is <= 0, the header will also be stripped.
|
||||
var BuildContentLengthHandler = request.NamedHandler{Name: "core.BuildContentLengthHandler", Fn: func(r *request.Request) {
|
||||
var length int64
|
||||
|
||||
if slength := r.HTTPRequest.Header.Get("Content-Length"); slength != "" {
|
||||
length, _ = strconv.ParseInt(slength, 10, 64)
|
||||
} else {
|
||||
if r.Body != nil {
|
||||
var err error
|
||||
length, err = aws.SeekerLen(r.Body)
|
||||
if err != nil {
|
||||
r.Error = awserr.New(request.ErrCodeSerialization, "failed to get request body's length", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
r.HTTPRequest.ContentLength = length
|
||||
r.HTTPRequest.Header.Set("Content-Length", fmt.Sprintf("%d", length))
|
||||
} else {
|
||||
r.HTTPRequest.ContentLength = 0
|
||||
r.HTTPRequest.Header.Del("Content-Length")
|
||||
}
|
||||
}}
|
||||
|
||||
var reStatusCode = regexp.MustCompile(`^(\d{3})`)
|
||||
|
||||
// ValidateReqSigHandler is a request handler to ensure that the request's
|
||||
// signature doesn't expire before it is sent. This can happen when a request
|
||||
// is built and signed significantly before it is sent. Or significant delays
|
||||
// occur when retrying requests that would cause the signature to expire.
|
||||
var ValidateReqSigHandler = request.NamedHandler{
|
||||
Name: "core.ValidateReqSigHandler",
|
||||
Fn: func(r *request.Request) {
|
||||
// Unsigned requests are not signed
|
||||
if r.Config.Credentials == credentials.AnonymousCredentials {
|
||||
return
|
||||
}
|
||||
|
||||
signedTime := r.Time
|
||||
if !r.LastSignedAt.IsZero() {
|
||||
signedTime = r.LastSignedAt
|
||||
}
|
||||
|
||||
// 10 minutes to allow for some clock skew/delays in transmission.
|
||||
// Would be improved with aws/aws-sdk-go#423
|
||||
if signedTime.Add(10 * time.Minute).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("request expired, resigning")
|
||||
r.Sign()
|
||||
},
|
||||
}
|
||||
|
||||
// SendHandler is a request handler to send service request using HTTP client.
|
||||
var SendHandler = request.NamedHandler{
|
||||
Name: "core.SendHandler",
|
||||
Fn: func(r *request.Request) {
|
||||
sender := sendFollowRedirects
|
||||
if r.DisableFollowRedirects {
|
||||
sender = sendWithoutFollowRedirects
|
||||
}
|
||||
|
||||
if request.NoBody == r.HTTPRequest.Body {
|
||||
// Strip off the request body if the NoBody reader was used as a
|
||||
// place holder for a request body. This prevents the SDK from
|
||||
// making requests with a request body when it would be invalid
|
||||
// to do so.
|
||||
//
|
||||
// Use a shallow copy of the http.Request to ensure the race condition
|
||||
// of transport on Body will not trigger
|
||||
reqOrig, reqCopy := r.HTTPRequest, *r.HTTPRequest
|
||||
reqCopy.Body = nil
|
||||
r.HTTPRequest = &reqCopy
|
||||
defer func() {
|
||||
r.HTTPRequest = reqOrig
|
||||
}()
|
||||
}
|
||||
|
||||
var err error
|
||||
r.HTTPResponse, err = sender(r)
|
||||
if err != nil {
|
||||
handleSendError(r, err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func sendFollowRedirects(r *request.Request) (*http.Response, error) {
|
||||
return r.Config.HTTPClient.Do(r.HTTPRequest)
|
||||
}
|
||||
|
||||
func sendWithoutFollowRedirects(r *request.Request) (*http.Response, error) {
|
||||
transport := r.Config.HTTPClient.Transport
|
||||
if transport == nil {
|
||||
transport = http.DefaultTransport
|
||||
}
|
||||
|
||||
return transport.RoundTrip(r.HTTPRequest)
|
||||
}
|
||||
|
||||
func handleSendError(r *request.Request, err error) {
|
||||
// Prevent leaking if an HTTPResponse was returned. Clean up
|
||||
// the body.
|
||||
if r.HTTPResponse != nil {
|
||||
r.HTTPResponse.Body.Close()
|
||||
}
|
||||
// Capture the case where url.Error is returned for error processing
|
||||
// response. e.g. 301 without location header comes back as string
|
||||
// error and r.HTTPResponse is nil. Other URL redirect errors will
|
||||
// comeback in a similar method.
|
||||
if e, ok := err.(*url.Error); ok && e.Err != nil {
|
||||
if s := reStatusCode.FindStringSubmatch(e.Err.Error()); s != nil {
|
||||
code, _ := strconv.ParseInt(s[1], 10, 64)
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: int(code),
|
||||
Status: http.StatusText(int(code)),
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.HTTPResponse == nil {
|
||||
// Add a dummy request response object to ensure the HTTPResponse
|
||||
// value is consistent.
|
||||
r.HTTPResponse = &http.Response{
|
||||
StatusCode: int(0),
|
||||
Status: http.StatusText(int(0)),
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
}
|
||||
}
|
||||
// Catch all other request errors.
|
||||
r.Error = awserr.New("RequestError", "send request failed", err)
|
||||
r.Retryable = aws.Bool(true) // network errors are retryable
|
||||
|
||||
// Override the error with a context canceled error, if that was canceled.
|
||||
ctx := r.Context()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.Error = awserr.New(request.CanceledErrorCode,
|
||||
"request context canceled", ctx.Err())
|
||||
r.Retryable = aws.Bool(false)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateResponseHandler is a request handler to validate service response.
|
||||
var ValidateResponseHandler = request.NamedHandler{Name: "core.ValidateResponseHandler", Fn: func(r *request.Request) {
|
||||
if r.HTTPResponse.StatusCode == 0 || r.HTTPResponse.StatusCode >= 300 {
|
||||
// this may be replaced by an UnmarshalError handler
|
||||
r.Error = awserr.New("UnknownError", "unknown error", nil)
|
||||
}
|
||||
}}
|
||||
|
||||
// AfterRetryHandler performs final checks to determine if the request should
|
||||
// be retried and how long to delay.
|
||||
var AfterRetryHandler = request.NamedHandler{Name: "core.AfterRetryHandler", Fn: func(r *request.Request) {
|
||||
// If one of the other handlers already set the retry state
|
||||
// we don't want to override it based on the service's state
|
||||
if r.Retryable == nil || aws.BoolValue(r.Config.EnforceShouldRetryCheck) {
|
||||
r.Retryable = aws.Bool(r.ShouldRetry(r))
|
||||
}
|
||||
|
||||
if r.WillRetry() {
|
||||
r.RetryDelay = r.RetryRules(r)
|
||||
|
||||
if sleepFn := r.Config.SleepDelay; sleepFn != nil {
|
||||
// Support SleepDelay for backwards compatibility and testing
|
||||
sleepFn(r.RetryDelay)
|
||||
} else if err := aws.SleepWithContext(r.Context(), r.RetryDelay); err != nil {
|
||||
r.Error = awserr.New(request.CanceledErrorCode,
|
||||
"request context canceled", err)
|
||||
r.Retryable = aws.Bool(false)
|
||||
return
|
||||
}
|
||||
|
||||
// when the expired token exception occurs the credentials
|
||||
// need to be expired locally so that the next request to
|
||||
// get credentials will trigger a credentials refresh.
|
||||
if r.IsErrorExpired() {
|
||||
r.Config.Credentials.Expire()
|
||||
}
|
||||
|
||||
r.RetryCount++
|
||||
r.Error = nil
|
||||
}
|
||||
}}
|
||||
|
||||
// ValidateEndpointHandler is a request handler to validate a request had the
|
||||
// appropriate Region and Endpoint set. Will set r.Error if the endpoint or
|
||||
// region is not valid.
|
||||
var ValidateEndpointHandler = request.NamedHandler{Name: "core.ValidateEndpointHandler", Fn: func(r *request.Request) {
|
||||
if r.ClientInfo.SigningRegion == "" && aws.StringValue(r.Config.Region) == "" {
|
||||
r.Error = aws.ErrMissingRegion
|
||||
} else if r.ClientInfo.Endpoint == "" {
|
||||
r.Error = aws.ErrMissingEndpoint
|
||||
}
|
||||
}}
|
||||
17
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/param_validator.go
generated
vendored
Normal file
17
vendor/github.com/aws/aws-sdk-go/aws/corehandlers/param_validator.go
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
package corehandlers
|
||||
|
||||
import "github.com/aws/aws-sdk-go/aws/request"
|
||||
|
||||
// ValidateParametersHandler is a request handler to validate the input parameters.
|
||||
// Validating parameters only has meaning if done prior to the request being sent.
|
||||
var ValidateParametersHandler = request.NamedHandler{Name: "core.ValidateParametersHandler", Fn: func(r *request.Request) {
|
||||
if !r.ParamsFilled() {
|
||||
return
|
||||
}
|
||||
|
||||
if v, ok := r.Params.(request.Validator); ok {
|
||||
if err := v.Validate(); err != nil {
|
||||
r.Error = err
|
||||
}
|
||||
}
|
||||
}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user