Compare commits
719 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a607061a2 | ||
|
|
695b092c41 | ||
|
|
a38d1a3b68 | ||
|
|
2dc5e6d503 | ||
|
|
0dcfa09ff2 | ||
|
|
d5401ab200 | ||
|
|
b6f8ebc0ca | ||
|
|
3e24069722 | ||
|
|
c722ea5afc | ||
|
|
c759c7ac65 | ||
|
|
e50bd812fc | ||
|
|
7ff022f1e7 | ||
|
|
1db8801771 | ||
|
|
666ea3a4a0 | ||
|
|
47d50989c4 | ||
|
|
e4f70278dd | ||
|
|
0afffd03ca | ||
|
|
6c5e409ffa | ||
|
|
800b40ecc4 | ||
|
|
097f687efe | ||
|
|
aa30e00643 | ||
|
|
cf56dcf9ff | ||
|
|
c14a4eed0e | ||
|
|
a1b688f070 | ||
|
|
4793232a35 | ||
|
|
7835fce708 | ||
|
|
535152e15e | ||
|
|
160af3bb99 | ||
|
|
328b57e2cf | ||
|
|
20a94447d7 | ||
|
|
865c7c2332 | ||
|
|
11c7cbe3ac | ||
|
|
276ac3a92e | ||
|
|
a4beabf4b9 | ||
|
|
c35255b7a9 | ||
|
|
319064f040 | ||
|
|
f5f726e9c4 | ||
|
|
c56b303b29 | ||
|
|
4886b8350e | ||
|
|
af26b5f3e0 | ||
|
|
70cd6700e7 | ||
|
|
d11f8989d9 | ||
|
|
0fca27d022 | ||
|
|
255319e597 | ||
|
|
5d038dfd33 | ||
|
|
0577d3b97f | ||
|
|
a26c15dafa | ||
|
|
c71bcc64ed | ||
|
|
822dc5dada | ||
|
|
e20d8366e1 | ||
|
|
76e9582739 | ||
|
|
50f20de8f3 | ||
|
|
8e3f5e19e0 | ||
|
|
61c2778de1 | ||
|
|
3c17bf761a | ||
|
|
696d6dc20c | ||
|
|
f14effe5f5 | ||
|
|
b95abd95ef | ||
|
|
ea6712dec8 | ||
|
|
de37a66ef3 | ||
|
|
efb82a58ae | ||
|
|
19a6a32625 | ||
|
|
270658fc00 | ||
|
|
ff856b7630 | ||
|
|
ca3afa2a39 | ||
|
|
99a8b1ae8b | ||
|
|
ccc771d8b1 | ||
|
|
cf5a85b80f | ||
|
|
2f7bd2896c | ||
|
|
8f904ffd72 | ||
|
|
ced81e11f0 | ||
|
|
6d0fa8bc29 | ||
|
|
21a808a52b | ||
|
|
89c272eed5 | ||
|
|
1b6d34e76a | ||
|
|
6711543634 | ||
|
|
f6e83cdbdf | ||
|
|
3b51d7cd00 | ||
|
|
66512ca253 | ||
|
|
1a6a69a8f1 | ||
|
|
933874fb25 | ||
|
|
c0f9795910 | ||
|
|
658e5a9faf | ||
|
|
99824c8a7b | ||
|
|
60060551bf | ||
|
|
c269ad1370 | ||
|
|
2edd2b74ff | ||
|
|
181f91d2ef | ||
|
|
643cdd3461 | ||
|
|
5c70d2724b | ||
|
|
55712f509c | ||
|
|
d91493b587 | ||
|
|
9da1382e09 | ||
|
|
4e8e4612bd | ||
|
|
adfc00bcdc | ||
|
|
b0eaf507a5 | ||
|
|
b9ecb82cb7 | ||
|
|
448d9caf1b | ||
|
|
6d2bf0b0b5 | ||
|
|
5160668efd | ||
|
|
0eb1e4a86b | ||
|
|
0c4c00c1bf | ||
|
|
cc7d78f1ee | ||
|
|
b8d5adcb84 | ||
|
|
a5f483fae9 | ||
|
|
775d910bdc | ||
|
|
18a1070c2c | ||
|
|
9fafd7ebc1 | ||
|
|
bc14b01d03 | ||
|
|
80c6e0a8c4 | ||
|
|
8742c4c110 | ||
|
|
32ecc6d745 | ||
|
|
834e42897d | ||
|
|
500267417b | ||
|
|
18bcc0df4d | ||
|
|
5ae0e75e5e | ||
|
|
1fd8cadd9e | ||
|
|
9d79d32c94 | ||
|
|
17b4b4cb33 | ||
|
|
79ef98739d | ||
|
|
c2eaeab1f0 | ||
|
|
32d1289af7 | ||
|
|
ea55643cb2 | ||
|
|
dcb6216713 | ||
|
|
9c8b241292 | ||
|
|
7c4d360645 | ||
|
|
ad77ac639e | ||
|
|
cf1e9f79b1 | ||
|
|
8f0741a458 | ||
|
|
6f2b62f729 | ||
|
|
8469239d84 | ||
|
|
af54d7f015 | ||
|
|
cb9ad5bc73 | ||
|
|
5470bb4121 | ||
|
|
0e53a26d6f | ||
|
|
3938138ebc | ||
|
|
05f0e5120a | ||
|
|
5532289086 | ||
|
|
78b2bc4f60 | ||
|
|
d33f89fd60 | ||
|
|
c0da212f54 | ||
|
|
9585f49490 | ||
|
|
1e0310a86d | ||
|
|
bf45e5b0e3 | ||
|
|
9a0f094f58 | ||
|
|
ee89ad6ae7 | ||
|
|
22e5aafd59 | ||
|
|
abd0803ef4 | ||
|
|
372b333662 | ||
|
|
18f09a14e6 | ||
|
|
ed564adb4a | ||
|
|
9a99748d3b | ||
|
|
9163110640 | ||
|
|
6c1c110ce0 | ||
|
|
45c249acca | ||
|
|
1df1053947 | ||
|
|
14cff0bd07 | ||
|
|
87d1b9a547 | ||
|
|
959d6fa2ca | ||
|
|
e47c597b3a | ||
|
|
bfcb348923 | ||
|
|
c6da4c8a47 | ||
|
|
1fedda6a75 | ||
|
|
467775fc1c | ||
|
|
3b6b68a2a1 | ||
|
|
3a23cb87b7 | ||
|
|
ac03665df3 | ||
|
|
1be44eae84 | ||
|
|
b72841ca0c | ||
|
|
12425f0aa7 | ||
|
|
727ba9f42e | ||
|
|
73a0a65ee1 | ||
|
|
ac5696574c | ||
|
|
1a43d64de3 | ||
|
|
990dc8c4ea | ||
|
|
59cdd7d46e | ||
|
|
4451cbc50b | ||
|
|
01fa106de3 | ||
|
|
9fc4262887 | ||
|
|
cecd5733a8 | ||
|
|
1d733f3adc | ||
|
|
c69fce2e9d | ||
|
|
df0e3e52fe | ||
|
|
d5f64602a8 | ||
|
|
4287f8ae90 | ||
|
|
190309e5c1 | ||
|
|
ac65586bd5 | ||
|
|
af8d362caa | ||
|
|
5f7ac97a39 | ||
|
|
b8b59baa27 | ||
|
|
2be613679e | ||
|
|
28fe3d6cf9 | ||
|
|
b6b21bc98e | ||
|
|
eb69d98f99 | ||
|
|
fb9596a3ff | ||
|
|
0d33a746ba | ||
|
|
f3fc98a3d0 | ||
|
|
17d7bcdeaf | ||
|
|
d0a3f1eecf | ||
|
|
7164f37266 | ||
|
|
e9245cd53b | ||
|
|
80d6bbef86 | ||
|
|
3d751c03fe | ||
|
|
4e0d0f7d75 | ||
|
|
b8b3eee961 | ||
|
|
7947668e18 | ||
|
|
619c28ce56 | ||
|
|
53aef7846a | ||
|
|
227067fdd1 | ||
|
|
3df4a9484f | ||
|
|
2229a6e133 | ||
|
|
3101c50582 | ||
|
|
70ee4faf15 | ||
|
|
360b7c1def | ||
|
|
bdeb78c9a0 | ||
|
|
9481920101 | ||
|
|
a2b3cd0823 | ||
|
|
8fac19c175 | ||
|
|
b9708c9f88 | ||
|
|
7b90d2496b | ||
|
|
0367399cf3 | ||
|
|
3072c93e13 | ||
|
|
4ea446205c | ||
|
|
5a76b57952 | ||
|
|
6de291ff44 | ||
|
|
64f0eeb42e | ||
|
|
baa9eff318 | ||
|
|
fcaf4e339c | ||
|
|
e91fb21233 | ||
|
|
99a6439641 | ||
|
|
768b9453f8 | ||
|
|
e95b2e5f0b | ||
|
|
950cfeff6f | ||
|
|
fce895ed0d | ||
|
|
c789bba673 | ||
|
|
b384fcf6af | ||
|
|
60cf549a32 | ||
|
|
6f0b32f95e | ||
|
|
f89bc10af1 | ||
|
|
a66ac8092e | ||
|
|
bd04ecff69 | ||
|
|
c00c834b35 | ||
|
|
9d9d775f50 | ||
|
|
38036e0d20 | ||
|
|
9713a15167 | ||
|
|
b641d6bd96 | ||
|
|
67a42f49b4 | ||
|
|
bbc88071e9 | ||
|
|
ca2eec60fe | ||
|
|
c1b7a21631 | ||
|
|
91832f2c5e | ||
|
|
c64fb87b2b | ||
|
|
fa08c6c2a2 | ||
|
|
3cf84a5af1 | ||
|
|
61f0801bd3 | ||
|
|
eb4b5cd43b | ||
|
|
c92510ceba | ||
|
|
65a24d70c3 | ||
|
|
7fb2cafd0c | ||
|
|
3b765e5417 | ||
|
|
57f6a552d2 | ||
|
|
35cae80de9 | ||
|
|
b4b4cd83dd | ||
|
|
31c33dfdcb | ||
|
|
79940b7ba9 | ||
|
|
2ce8ac5850 | ||
|
|
f8b484f638 | ||
|
|
73e2c1005a | ||
|
|
97e0a6dc45 | ||
|
|
9bad0337fe | ||
|
|
f03544f392 | ||
|
|
0aba49af2b | ||
|
|
ccbc5e569c | ||
|
|
415aad600c | ||
|
|
d23577168f | ||
|
|
5c204b2813 | ||
|
|
7d86278507 | ||
|
|
985196f5aa | ||
|
|
52b132fe01 | ||
|
|
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 | ||
|
|
ca715c5b23 | ||
|
|
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 |
73
.circleci/config.yml
Normal file
73
.circleci/config.yml
Normal file
@@ -0,0 +1,73 @@
|
||||
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" }}-v3
|
||||
- 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" }}-v3
|
||||
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.
|
||||
33
.gitignore
vendored
33
.gitignore
vendored
@@ -1,7 +1,26 @@
|
||||
development.log
|
||||
commands.log
|
||||
extra/lgit.rb
|
||||
notes/go.notes
|
||||
TODO.notes
|
||||
TODO.md
|
||||
test/testrepo/
|
||||
# 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,23 +1,28 @@
|
||||
# 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
|
||||
@@ -25,13 +30,14 @@ archive:
|
||||
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:
|
||||
@@ -40,20 +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
|
||||
# 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].
|
||||
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh -l
|
||||
|
||||
FROM golang:alpine
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY ./ .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o lazygit .
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk add -U git xdg-utils
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
RUN echo "alias gg=lazygit" >> ~/.profile
|
||||
366
Gopkg.lock
generated
366
Gopkg.lock
generated
@@ -1,6 +1,62 @@
|
||||
# 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"
|
||||
@@ -32,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"
|
||||
@@ -40,6 +112,65 @@
|
||||
pruneopts = "NUT"
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
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"
|
||||
@@ -50,11 +181,50 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e9b2b07a20f19d886267876b72ba15f2cbdeeeadd18030a4ce174b864e97c39e"
|
||||
digest = "1:490643e333b848f3d6ab772c21082d706663dcf4a3c0fbe9a4b4ef7b205ce6c7"
|
||||
name = "github.com/jesseduffield/go-getter"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "906e15686e6309ff310c1c10463ab53287c3a678"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9b266d7748a5d94985fd9e323494f5b8ae1ab3e910418e898dfe7f03339ddbcd"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "8cecad864fb0b099a5f55bf1c97fbc1daca103e0"
|
||||
revision = "cfa9e452ba5ebf014041846851152d64a59dce14"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a46c2f4863e5284ddb255c28750298e04bc8c0fc896bed6056e947673168b7be"
|
||||
name = "github.com/jesseduffield/pty"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "02db52c7e406c7abec44c717a173c7715e4c1b62"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3ab130f65766f5b7cc944d557df31c6a007ec017151705ec1e1b8719f2689021"
|
||||
name = "github.com/jesseduffield/termbox-go"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "1e272ff78dcb4c448870f464fda1cdcf2bf0b3dd"
|
||||
|
||||
[[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"
|
||||
@@ -64,6 +234,14 @@
|
||||
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"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
@@ -88,6 +266,14 @@
|
||||
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"
|
||||
@@ -98,11 +284,31 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:34d9354c2c5d916c05864327553047df59fc10e86ff1f408e4136eba0a25a5ec"
|
||||
name = "github.com/nsf/termbox-go"
|
||||
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
|
||||
name = "github.com/mitchellh/go-testing-interface"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
|
||||
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]]
|
||||
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
|
||||
@@ -112,6 +318,30 @@
|
||||
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"
|
||||
@@ -120,6 +350,72 @@
|
||||
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"
|
||||
@@ -133,6 +429,22 @@
|
||||
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"
|
||||
@@ -141,6 +453,19 @@
|
||||
revision = "d154598bacbf4501c095a309753c5d4af66caa81"
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:07e8742c479bab0066149ad02a710024154e76874fd0a2dba002d87702725825"
|
||||
name = "github.com/ulikunitz/xz"
|
||||
packages = [
|
||||
".",
|
||||
"internal/hash",
|
||||
"internal/xlog",
|
||||
"lzma",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "0c6b41e72360850ca4f98dc341fd999726ea007f"
|
||||
version = "v0.5.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3148cb3478c26a92b4c1a18abb9428234b281e278af6267840721a24b6cbc6a3"
|
||||
name = "github.com/xanzy/ssh-agent"
|
||||
@@ -151,7 +476,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c76f8b24a4d9b99b502fb7b61ad769125075cb570efff9b9b73e6c428629532d"
|
||||
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cast5",
|
||||
@@ -170,6 +495,7 @@
|
||||
"ssh",
|
||||
"ssh/agent",
|
||||
"ssh/knownhosts",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||
@@ -194,12 +520,14 @@
|
||||
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295"
|
||||
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
@@ -278,18 +606,38 @@
|
||||
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/davecgh/go-spew/spew",
|
||||
"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/jesseduffield/pty",
|
||||
"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/src-d/go-git.v4/plumbing/object",
|
||||
"gopkg.in/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
branch = "master"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jesseduffield/pty"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spkg/bom"
|
||||
|
||||
46
README.md
46
README.md
@@ -1,4 +1,4 @@
|
||||
# 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.
|
||||
|
||||
@@ -7,14 +7,14 @@ too stubborn to use Sourcetree because you'll never forgive Atlassian for making
|
||||
Jira? This is the app for you!
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
* [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)
|
||||
* [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
|
||||
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
|
||||
## Installation
|
||||
@@ -28,7 +28,19 @@ brew install lazygit
|
||||
### Ubuntu
|
||||
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
|
||||
@@ -57,6 +69,12 @@ and the git version which builds from the most recent commit.
|
||||
Instruction of how to install AUR content can be found here:
|
||||
https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
|
||||
### Conda
|
||||
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
|
||||
```sh
|
||||
conda install -c conda-forge lazygit
|
||||
```
|
||||
|
||||
### Binary Release (Windows/Linux/OSX)
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
|
||||
|
||||
@@ -76,9 +94,9 @@ 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 video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
|
||||
* Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
|
||||
* List of keybindings
|
||||
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md).
|
||||
[here](/docs/Keybindings.md).
|
||||
|
||||
## Cool features
|
||||
* Adding files easily
|
||||
@@ -89,10 +107,10 @@ whichever rc file you're using).
|
||||
* Squash down and rename commits
|
||||
|
||||
### Resolving merge conflicts
|
||||

|
||||

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

|
||||

|
||||
|
||||
## Milestones
|
||||
- [x] Easy Installation (homebrew, release binaries)
|
||||
@@ -105,6 +123,14 @@ whichever rc file you're using).
|
||||
|
||||
## 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)
|
||||
|
||||
## Donate
|
||||
If you would like to support the development of lazygit, please donate
|
||||
|
||||
[](https://donorbox.org/lazygit)
|
||||
|
||||
## Work in progress
|
||||
This is still a work in progress so there's still bugs to iron out and as this
|
||||
@@ -116,3 +142,7 @@ feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[
|
||||
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)
|
||||
|
||||
35
branch.go
35
branch.go
@@ -1,35 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Branch : A git branch
|
||||
type Branch struct {
|
||||
Name string
|
||||
Recency string
|
||||
}
|
||||
|
||||
func (b *Branch) getDisplayString() string {
|
||||
return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor())
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
@@ -1,135 +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 handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
checkedOutBranch := state.Branches[0]
|
||||
selectedBranch := getSelectedBranch(v)
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return createErrorPanel(g, "You cannot delete the checked out branch!")
|
||||
}
|
||||
return createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitDeleteBranch(selectedBranch.Name); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
return refreshSidePanels(g)
|
||||
}, 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",
|
||||
"d": "delete 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)
|
||||
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.getDisplayString())
|
||||
}
|
||||
resetOrigin(v)
|
||||
return refreshStatus(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func handleCommitConfirm(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 {
|
||||
if err == errNoUsername {
|
||||
return createErrorPanel(g, err.Error())
|
||||
}
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
refreshFiles(g)
|
||||
v.Clear()
|
||||
v.SetCursor(0, 0)
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
switchFocus(g, v, getFilesView(g))
|
||||
return refreshCommits(g)
|
||||
}
|
||||
|
||||
func handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
return switchFocus(g, v, getFilesView(g))
|
||||
}
|
||||
|
||||
func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
|
||||
// resising ahead of time so that the top line doesn't get hidden to make
|
||||
// room for the cursor on the second line
|
||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
||||
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v.EditNewLine()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
return renderString(g, "options", "esc: close, enter: confirm")
|
||||
}
|
||||
176
commits_panel.go
176
commits_panel.go
@@ -1,176 +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)
|
||||
if g.CurrentView().Name() == "commits" {
|
||||
handleCommitSelect(g, v)
|
||||
}
|
||||
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)
|
||||
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",
|
||||
"f": "fixup 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)
|
||||
}
|
||||
|
||||
// TODO: move to files panel
|
||||
func anyUnStagedChanges(files []GitFile) bool {
|
||||
for _, file := range files {
|
||||
if file.Tracked && file.HasUnstagedChanges {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
if len(state.Commits) == 1 {
|
||||
return createErrorPanel(g, "You have no commits to squash with")
|
||||
}
|
||||
objectLog(state.GitFiles)
|
||||
if anyUnStagedChanges(state.GitFiles) {
|
||||
return createErrorPanel(g, "Can't fixup while there are unstaged changes")
|
||||
}
|
||||
branch := state.Branches[0]
|
||||
commit, err := getSelectedCommit(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if output, err := gitSquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
||||
return createErrorPanel(g, output)
|
||||
}
|
||||
if err := refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return refreshStatus(g)
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
devLog("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,155 +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 := width / 2
|
||||
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, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
// 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
|
||||
}
|
||||
|
||||
confirmationView.Editable = true
|
||||
confirmationView.Title = title
|
||||
switchFocus(g, currentView, confirmationView)
|
||||
return setKeyBindings(g, handleConfirm, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
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, handleConfirm, handleClose)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleNewline(g *gocui.Gui, v *gocui.View) error {
|
||||
// resising ahead of time so that the top line doesn't get hidden to make
|
||||
// room for the cursor on the second line
|
||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
||||
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
v.EditNewLine()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
renderString(g, "options", "esc: close, enter: confirm")
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, handleNewline); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleClose))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func trimTrailingNewline(str string) string {
|
||||
if strings.HasSuffix(str, "\n") {
|
||||
return str[:len(str)-1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
// If the confirmation panel is already displayed, just resize the width,
|
||||
// otherwise continue
|
||||
content := trimTrailingNewline(v.Buffer())
|
||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content)
|
||||
vx0, vy0, vx1, vy1 := v.Dimensions()
|
||||
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
||||
return nil
|
||||
}
|
||||
devLog("resizing popup panel")
|
||||
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||
return err
|
||||
}
|
||||
79
docs/Config.md
Normal file
79
docs/Config.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# User Config:
|
||||
|
||||
## Default:
|
||||
|
||||
```
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
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:
|
||||
|
||||

|
||||
@@ -1,17 +1,28 @@
|
||||
# Keybindings:
|
||||
|
||||
## Global:
|
||||
|
||||
<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>: scroll diff panel (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
|
||||
<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:
|
||||
|
||||
<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
|
||||
@@ -23,9 +34,11 @@
|
||||
<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:
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout branch
|
||||
<kbd>f</kbd>: force checkout branch
|
||||
@@ -33,16 +46,20 @@
|
||||
<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:
|
||||
|
||||
<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:
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
@@ -50,6 +67,7 @@
|
||||
</pre>
|
||||
|
||||
## Popup Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close/cancel
|
||||
<kbd>enter</kbd>: confirm
|
||||
@@ -57,6 +75,7 @@
|
||||
</pre>
|
||||
|
||||
## Resolving Merge Conflicts (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
|
||||
|
||||
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 |
362
files_panel.go
362
files_panel.go
@@ -1,362 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoFiles = errors.New("No changed files")
|
||||
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
|
||||
)
|
||||
|
||||
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",
|
||||
"PgUp/PgDn": "scroll",
|
||||
}
|
||||
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")
|
||||
}
|
||||
commitMessageView := getCommitMessageView(g)
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
g.SetViewOnTop("commitMessage")
|
||||
switchFocus(g, filesView, commitMessageView)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleCommitEditorPress(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")
|
||||
}
|
||||
runSubProcess(g, "git", "commit")
|
||||
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 {
|
||||
createMessagePanel(g, v, "", "Pulling...")
|
||||
go func() {
|
||||
if output, err := gitPull(); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
} else {
|
||||
closeConfirmationPrompt(g)
|
||||
refreshCommits(g)
|
||||
refreshStatus(g)
|
||||
}
|
||||
refreshFiles(g)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
createMessagePanel(g, v, "", "Pushing...")
|
||||
go func() {
|
||||
if output, err := gitPush(); err != nil {
|
||||
createErrorPanel(g, output)
|
||||
} else {
|
||||
closeConfirmationPrompt(g)
|
||||
refreshCommits(g)
|
||||
refreshStatus(g)
|
||||
}
|
||||
}()
|
||||
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)
|
||||
}
|
||||
|
||||
func handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
||||
return createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gitResetHard(); err != nil {
|
||||
createErrorPanel(g, err.Error())
|
||||
}
|
||||
return refreshFiles(g)
|
||||
}, nil)
|
||||
}
|
||||
546
gitcommands.go
546
gitcommands.go
@@ -1,546 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
git "gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/object"
|
||||
)
|
||||
|
||||
var (
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// only to be used when you're already in an error state
|
||||
func runDirectCommandIgnoringError(command string) string {
|
||||
output, _ := runDirectCommand(command)
|
||||
return output
|
||||
}
|
||||
|
||||
func runDirectCommand(command string) (string, error) {
|
||||
commandLog(command)
|
||||
|
||||
cmdOut, err := exec.
|
||||
Command(state.Platform.shell, state.Platform.shellArg, command).
|
||||
CombinedOutput()
|
||||
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]
|
||||
}
|
||||
|
||||
// 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",
|
||||
}
|
||||
gitFiles = append(gitFiles, gitFile)
|
||||
}
|
||||
objectLog(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) {
|
||||
commandLog(command)
|
||||
splitCmd := strings.Split(command, " ")
|
||||
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
|
||||
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 == "" {
|
||||
if _, err := runCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
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) (string, error) {
|
||||
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + 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 "
|
||||
}
|
||||
_, 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, "git", "commit")
|
||||
return "", nil
|
||||
}
|
||||
userName, err := gitconfig.Username()
|
||||
if userName == "" {
|
||||
return "", errNoUsername
|
||||
}
|
||||
userEmail, err := gitconfig.Email()
|
||||
_, err = w.Commit(message, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: userName,
|
||||
Email: userEmail,
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error(), err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func gitPull() (string, error) {
|
||||
return runDirectCommand("git pull --no-edit")
|
||||
}
|
||||
|
||||
func gitPush() (string, error) {
|
||||
return runDirectCommand("git push -u origin " + state.Branches[0].Name)
|
||||
}
|
||||
|
||||
func gitSquashPreviousTwoCommits(message string) (string, error) {
|
||||
return runDirectCommand("git reset --soft HEAD^ && git commit --amend -m \"" + message + "\"")
|
||||
}
|
||||
|
||||
func gitSquashFixupCommit(branchName string, shaValue string) (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 {
|
||||
devLog(command)
|
||||
output, err := runDirectCommand(command)
|
||||
ret += output
|
||||
if err != nil {
|
||||
devLog(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
|
||||
ret += runDirectCommandIgnoringError("git branch -d " + shaValue)
|
||||
ret += runDirectCommandIgnoringError("git checkout " + branchName)
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
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 gitDeleteBranch(branch string) (string, error) {
|
||||
return runCommand("git branch -d " + branch)
|
||||
}
|
||||
|
||||
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 getGitBranches() []Branch {
|
||||
builder := newBranchListBuilder()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func gitResetHard() error {
|
||||
return w.Reset(&git.ResetOptions{Mode: git.HardReset})
|
||||
}
|
||||
63
go.mod
Normal file
63
go.mod
Normal file
@@ -0,0 +1,63 @@
|
||||
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-20190115084758-cfa9e452ba5e
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb
|
||||
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/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
|
||||
)
|
||||
121
go.sum
Normal file
121
go.sum
Normal file
@@ -0,0 +1,121 @@
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
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/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/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/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/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/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-20181209104758-fe55a32c8a4c h1:jEfh/vAtfF3pQ8xFhpYR/0S4iHo11VfaYelJmzZJm94=
|
||||
github.com/jesseduffield/gocui v0.0.0-20181209104758-fe55a32c8a4c/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 h1:iYMH6h6SuWuBkIzRtymosE8NpSgTK0oRMfyTdVWgxzc=
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb h1:cFHYEWpQEfzFZVKiKZytCUX4UwQixKSw0kd3WhluPsY=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
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/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 h1:7ory6RlsEkeK89iyV7Imz3sVz8YHeSw29w3PehpCWC0=
|
||||
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.6 h1:SooTCzUOOs899x/M7gmSS+dgL+AUfSWqAcHLN3auCic=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
|
||||
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/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/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/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/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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.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/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=
|
||||
300
gui.go
300
gui.go
@@ -1,300 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
"runtime"
|
||||
"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
|
||||
Platform platform
|
||||
}
|
||||
|
||||
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(),
|
||||
Platform: getPlatform(),
|
||||
}
|
||||
|
||||
type platform struct {
|
||||
os string
|
||||
shell string
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
}
|
||||
|
||||
func getPlatform() platform {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return platform{
|
||||
os: "windows",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: "\\\"",
|
||||
}
|
||||
default:
|
||||
return platform{
|
||||
os: runtime.GOOS,
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "\"",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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 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
|
||||
minimumWidth := 10
|
||||
|
||||
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 = "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.FgColor = gocui.ColorBlue
|
||||
v.Frame = false
|
||||
}
|
||||
|
||||
if getCommitMessageView(g) == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = "Commit message"
|
||||
commitMessageView.FgColor = gocui.ColorWhite
|
||||
commitMessageView.Editable = true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
resizePopupPanels(g)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetch(g *gocui.Gui) error {
|
||||
gitFetch()
|
||||
refreshStatus(g)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLoader(g *gocui.Gui) error {
|
||||
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
||||
content := trimmedContent(confirmationView)
|
||||
if strings.Contains(content, "...") {
|
||||
staticContent := strings.Split(content, "...")[0] + "..."
|
||||
renderString(g, "confirmation", staticContent+" "+loader())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
|
||||
go func() {
|
||||
for range time.Tick(interval) {
|
||||
function(g)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func resizePopupPanels(g *gocui.Gui) error {
|
||||
v := g.CurrentView()
|
||||
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
|
||||
return resizePopupPanel(g, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func run() (err error) {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer g.Close()
|
||||
|
||||
g.FgColor = gocui.ColorDefault
|
||||
|
||||
goEvery(g, time.Second*60, fetch)
|
||||
goEvery(g, time.Second*10, refreshFiles)
|
||||
goEvery(g, time.Millisecond*10, updateLoader)
|
||||
|
||||
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,91 +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{
|
||||
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
|
||||
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
|
||||
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
||||
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
||||
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
|
||||
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
|
||||
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
|
||||
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
|
||||
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress},
|
||||
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
|
||||
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
|
||||
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
|
||||
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
|
||||
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
|
||||
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
|
||||
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
|
||||
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
|
||||
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
|
||||
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
|
||||
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
|
||||
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
|
||||
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
|
||||
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
|
||||
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
|
||||
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
|
||||
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
||||
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
||||
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
|
||||
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
||||
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
||||
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
||||
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
|
||||
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
||||
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
|
||||
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
|
||||
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
|
||||
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
|
||||
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
|
||||
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch},
|
||||
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
|
||||
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
|
||||
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
|
||||
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
|
||||
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
|
||||
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
|
||||
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
|
||||
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
|
||||
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm},
|
||||
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose},
|
||||
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage},
|
||||
}
|
||||
|
||||
// Would make these keybindings global but that interferes with editing
|
||||
// input in the confirmation panel
|
||||
for _, viewName := range []string{"files", "branches", "commits", "stash"} {
|
||||
bindings = append(bindings, []Binding{
|
||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
|
||||
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView},
|
||||
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView},
|
||||
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp},
|
||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown},
|
||||
}...)
|
||||
}
|
||||
|
||||
for _, binding := range bindings {
|
||||
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
134
main.go
134
main.go
@@ -1,138 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
git "gopkg.in/src-d/go-git.v4"
|
||||
"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
|
||||
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")
|
||||
|
||||
w *git.Worktree
|
||||
r *git.Repository
|
||||
)
|
||||
|
||||
func homeDirectory() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return usr.HomeDir
|
||||
}
|
||||
|
||||
func projectPath(path string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
||||
}
|
||||
|
||||
func devLog(objects ...interface{}) {
|
||||
localLog("development.log", objects...)
|
||||
}
|
||||
|
||||
func objectLog(object interface{}) {
|
||||
if !*debuggingFlag {
|
||||
return
|
||||
}
|
||||
str := spew.Sdump(object)
|
||||
localLog("development.log", str)
|
||||
}
|
||||
|
||||
func commandLog(objects ...interface{}) {
|
||||
localLog("commands.log", objects...)
|
||||
}
|
||||
|
||||
func localLog(path string, objects ...interface{}) {
|
||||
if !*debuggingFlag {
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(projectPath(path), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
defer f.Close()
|
||||
log.SetOutput(f)
|
||||
for _, object := range objects {
|
||||
log.Println(fmt.Sprint(object))
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// when building the binary, `version` is set as a compile-time variable, along
|
||||
// with `date` and `commit`. If this program has been opened directly via go,
|
||||
// we will populate the `version` with VERSION in the lazygit root directory
|
||||
func fallbackVersion() string {
|
||||
path := projectPath("VERSION")
|
||||
byteVersion, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "unversioned"
|
||||
}
|
||||
return string(byteVersion)
|
||||
}
|
||||
|
||||
func setupWorktree() {
|
||||
var err error
|
||||
r, err = git.PlainOpen(".")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
w, err = r.Worktree()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
devLog("\n\n\n\n\n\n\n\n\n\n")
|
||||
flag.Parse()
|
||||
if version == "unversioned" {
|
||||
version = fallbackVersion()
|
||||
}
|
||||
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()
|
||||
setupWorktree()
|
||||
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 {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app, err := app.Setup(appConfig)
|
||||
if err != nil {
|
||||
app.Log.Error(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app.Gui.RunWithSubprocesses()
|
||||
}
|
||||
|
||||
263
merge_panel.go
263
merge_panel.go
@@ -1,263 +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(coloredStringDirect(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 {
|
||||
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)
|
||||
}
|
||||
117
pkg/app/app.go
Normal file
117
pkg/app/app.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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/shibukawa/configdir"
|
||||
"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 globalConfigDir() string {
|
||||
configDirs := configdir.New("jesseduffield", "lazygit")
|
||||
configDir := configDirs.QueryFolders(configdir.Global)[0]
|
||||
return configDir.Path
|
||||
}
|
||||
|
||||
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
log := logrus.New()
|
||||
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "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(config)
|
||||
} else {
|
||||
log = newProductionLogger(config)
|
||||
}
|
||||
|
||||
// highly recommended: tail -f development.log | humanlog
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
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.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, 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
|
||||
}
|
||||
48
pkg/commands/branch.go
Normal file
48
pkg/commands/branch.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
Pushables string
|
||||
Pullables string
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
func (b *Branch) GetDisplayStrings() []string {
|
||||
displayName := utils.ColoredString(b.Name, b.GetColor())
|
||||
if b.Selected && b.Pushables != "" && b.Pullables != "" {
|
||||
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
|
||||
}
|
||||
|
||||
return []string{b.Recency, displayName}
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
31
pkg/commands/commit.go
Normal file
31
pkg/commands/commit.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Pushed bool
|
||||
Merged bool
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (c *Commit) GetDisplayStrings() []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgGreen)
|
||||
green := color.New(color.FgYellow)
|
||||
white := color.New(color.FgWhite)
|
||||
|
||||
shaColor := yellow
|
||||
if c.Pushed {
|
||||
shaColor = red
|
||||
} else if !c.Merged {
|
||||
shaColor = green
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
|
||||
}
|
||||
100
pkg/commands/exec_live_default.go
Normal file
100
pkg/commands/exec_live_default.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jesseduffield/pty"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
||||
// Output is a function that executes by every word that gets read by bufio
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
splitCmd := str.ToArgv(command)
|
||||
cmd := exec.Command(splitCmd[0], splitCmd[1:]...)
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(ptmx)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
toOutput := strings.Trim(scanner.Text(), " ")
|
||||
_, _ = ptmx.WriteString(output(toOutput))
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
ptmx.Close()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
9
pkg/commands/exec_live_win.go
Normal file
9
pkg/commands/exec_live_win.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package commands
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
return c.RunCommand(command)
|
||||
}
|
||||
36
pkg/commands/file.go
Normal file
36
pkg/commands/file.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
// File : A file from git status
|
||||
// duplicating this for now
|
||||
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'
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
func (f *File) GetDisplayStrings() []string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
|
||||
output := green.Sprint(f.DisplayString[0:1])
|
||||
output += red.Sprint(f.DisplayString[1:3])
|
||||
if f.HasUnstagedChanges {
|
||||
output += red.Sprint(f.Name)
|
||||
} else {
|
||||
output += green.Sprint(f.Name)
|
||||
}
|
||||
return []string{output}
|
||||
}
|
||||
629
pkg/commands/git.go
Normal file
629
pkg/commands/git.go
Normal file
@@ -0,0 +1,629 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
// 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,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
}, 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
appendedIndexes := []int{}
|
||||
|
||||
// retain position of files we already could see
|
||||
result := []*File{}
|
||||
for _, oldFile := range oldFiles {
|
||||
for newIndex, newFile := range newFiles {
|
||||
if oldFile.Name == newFile.Name {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append any new files to the end
|
||||
for index, newFile := range newFiles {
|
||||
if !includesInt(appendedIndexes, index) {
|
||||
result = append(result, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func includesInt(list []int, a int) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
func (c *GitCommand) ResetAndClean() error {
|
||||
if err := c.OSCommand.RunCommand("git reset --hard HEAD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
return c.GetCommitDifferences("HEAD", "@{u}")
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
|
||||
upstream := "origin" // hardcoded for now
|
||||
return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName))
|
||||
}
|
||||
|
||||
// GetCommitDifferences checks how many pushables/pullables there are for the
|
||||
// current branch
|
||||
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
|
||||
command := "git rev-list %s..%s --count"
|
||||
pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from))
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to))
|
||||
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, a map is returned to ease look up
|
||||
func (c *GitCommand) GetCommitsToPush() map[string]bool {
|
||||
pushables := map[string]bool{}
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
|
||||
if err != nil {
|
||||
return pushables
|
||||
}
|
||||
for _, p := range utils.SplitLines(o) {
|
||||
pushables[p] = true
|
||||
}
|
||||
|
||||
return 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(unamePassQuestion func(string) string, canAskForCredentials bool) error {
|
||||
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
|
||||
if canAskForCredentials {
|
||||
return unamePassQuestion(question)
|
||||
}
|
||||
return "\n"
|
||||
})
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// CurrentBranchName is a function.
|
||||
func (c *GitCommand) CurrentBranchName() (string, error) {
|
||||
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err != nil {
|
||||
branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return utils.TrimTrailingNewline(branchName), nil
|
||||
}
|
||||
|
||||
// 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, _ := c.getLocalGitConfig("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
||||
}
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) {
|
||||
amendParam := ""
|
||||
if amend {
|
||||
amendParam = " --amend"
|
||||
}
|
||||
command := fmt.Sprintf("git commit%s -m %s", amendParam, 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 pulls from repo
|
||||
func (c *GitCommand) Pull(ask func(string) string) error {
|
||||
return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease "
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName)
|
||||
return c.OSCommand.DetectUnamePass(cmd, ask)
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: if password is required, we need to return a subprocess
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", 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 {
|
||||
commands := []string{
|
||||
fmt.Sprintf("git checkout -q %s", shaValue),
|
||||
fmt.Sprintf("git reset --soft %s^", shaValue),
|
||||
fmt.Sprintf("git commit --amend -C %s^", shaValue),
|
||||
fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
|
||||
}
|
||||
for _, command := range commands {
|
||||
c.Log.Info(command)
|
||||
|
||||
if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil {
|
||||
ret := output
|
||||
// We are already in an error state here so we're just going to append
|
||||
// the output of these commands
|
||||
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue))
|
||||
ret += output
|
||||
output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName))
|
||||
ret += output
|
||||
|
||||
c.Log.Info(ret)
|
||||
return errors.New(ret)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", 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 {
|
||||
command := "git rm --cached %s"
|
||||
if tracked {
|
||||
command = "git reset HEAD %s"
|
||||
}
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf(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(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !file.Tracked {
|
||||
return c.removeFile(file.Name)
|
||||
}
|
||||
// if the file is tracked, we assume you want to just check it out
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", 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(fmt.Sprintf("git checkout %s %s", 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(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) getMergeBase() (string, error) {
|
||||
currentBranch, err := c.CurrentBranchName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseBranch := "master"
|
||||
if strings.HasPrefix(currentBranch, "feature/") {
|
||||
baseBranch = "develop"
|
||||
}
|
||||
|
||||
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
|
||||
if err != nil {
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *GitCommand) GetCommits() ([]*Commit, error) {
|
||||
pushables := c.GetCommitsToPush()
|
||||
log := c.GetLog()
|
||||
|
||||
lines := utils.SplitLines(log)
|
||||
commits := make([]*Commit, len(lines))
|
||||
// now we can split it up and turn it into commits
|
||||
for i, line := range lines {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
_, pushed := pushables[sha]
|
||||
commits[i] = &Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Pushed: pushed,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
}
|
||||
}
|
||||
return c.setCommitMergedStatuses(commits)
|
||||
}
|
||||
|
||||
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
ancestor, err := c.getMergeBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ancestor == "" {
|
||||
return commits, nil
|
||||
}
|
||||
passedAncestor := false
|
||||
for i, commit := range commits {
|
||||
if strings.HasPrefix(ancestor, commit.Sha) {
|
||||
passedAncestor = true
|
||||
}
|
||||
commits[i].Merged = passedAncestor
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// 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, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url")
|
||||
return utils.TrimTrailingNewline(url)
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File, plain bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := "--color"
|
||||
fileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = ""
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
|
||||
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(command)
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string) (string, error) {
|
||||
filename, err := c.OSCommand.CreateTempFile("patch", patch)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer func() { _ = c.OSCommand.RemoveFile(filename) }()
|
||||
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", filename))
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string) error {
|
||||
upstream := "origin" // hardcoding for now
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
|
||||
}
|
||||
9
pkg/commands/git_structs.go
Normal file
9
pkg/commands/git_structs.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package commands
|
||||
|
||||
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict bars appear
|
||||
type Conflict struct {
|
||||
Start int
|
||||
Middle int
|
||||
End int
|
||||
}
|
||||
2060
pkg/commands/git_test.go
Normal file
2060
pkg/commands/git_test.go
Normal file
File diff suppressed because it is too large
Load Diff
235
pkg/commands/os.go
Normal file
235
pkg/commands/os.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"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
|
||||
openLinkCommand 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(),
|
||||
)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password question in a command
|
||||
// ask is a function that gets executen when this function detect you need to fillin a password
|
||||
// The ask argument will be "username" or "password" and expects the user's password or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
|
||||
ttyText = ttyText + " " + word
|
||||
|
||||
prompts := map[string]string{
|
||||
"password": `Password\s*for\s*'.+':`,
|
||||
"username": `Username\s*for\s*'.+':`,
|
||||
}
|
||||
|
||||
for askFor, pattern := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return ask(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// OpenLink opens a file with the given
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// CreateTempFile writes a string to a new temp file and returns the file's name
|
||||
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
tmpfile, err := ioutil.TempFile("", filename)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
}
|
||||
|
||||
// RemoveFile removes a file at the specified path
|
||||
func (c *OSCommand) RemoveFile(filename string) error {
|
||||
return os.Remove(filename)
|
||||
}
|
||||
19
pkg/commands/os_default_platform.go
Normal file
19
pkg/commands/os_default_platform.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: runtime.GOOS,
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
openCommand: "open {{filename}}",
|
||||
openLinkCommand: "open {{link}}",
|
||||
fallbackEscapedQuote: "\"",
|
||||
}
|
||||
}
|
||||
398
pkg/commands/os_test.go
Normal file
398
pkg/commands/os_test.go
Normal file
@@ -0,0 +1,398 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"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
|
||||
}
|
||||
|
||||
// TestOSCommandRunCommandWithOutput is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandRunCommand is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFile is a function.
|
||||
func TestOSCommandOpenFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return 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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandEditFile is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
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)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteDoubleQuote 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)
|
||||
}
|
||||
|
||||
// TestOSCommandUnquote is a function.
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
|
||||
expected := "hello test"
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandFileType is a function.
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandCreateTempFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
content string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"filename",
|
||||
"content",
|
||||
func(path string, err error) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "content", string(content))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(newDummyOSCommand().CreateTempFile(s.filename, s.content))
|
||||
})
|
||||
}
|
||||
}
|
||||
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: "\\'",
|
||||
}
|
||||
}
|
||||
105
pkg/commands/pull_request.go
Normal file
105
pkg/commands/pull_request.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
PullRequestURL string
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
// with selected branch
|
||||
type PullRequest struct {
|
||||
GitServices []*Service
|
||||
GitCommand *GitCommand
|
||||
}
|
||||
|
||||
// RepoInformation holds some basic information about the repo
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
func getServices() []*Service {
|
||||
return []*Service{
|
||||
{
|
||||
Name: "github.com",
|
||||
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
|
||||
},
|
||||
{
|
||||
Name: "bitbucket.org",
|
||||
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?t=%s",
|
||||
},
|
||||
{
|
||||
Name: "gitlab.com",
|
||||
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewPullRequest creates new instance of PullRequest
|
||||
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
return &PullRequest{
|
||||
GitServices: getServices(),
|
||||
GitCommand: gitCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *Branch) error {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
|
||||
}
|
||||
|
||||
repoURL := pr.GitCommand.GetRemoteURL()
|
||||
var gitService *Service
|
||||
|
||||
for _, service := range pr.GitServices {
|
||||
if strings.Contains(repoURL, service.Name) {
|
||||
gitService = service
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if gitService == nil {
|
||||
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
))
|
||||
}
|
||||
|
||||
func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
isHTTP := strings.HasPrefix(url, "http")
|
||||
|
||||
if isHTTP {
|
||||
splits := strings.Split(url, "/")
|
||||
owner := splits[len(splits)-2]
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
tmpSplit := strings.Split(url, ":")
|
||||
splits := strings.Split(tmpSplit[1], "/")
|
||||
owner := splits[0]
|
||||
repo := strings.TrimSuffix(splits[1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
154
pkg/commands/pull_request_test.go
Normal file
154
pkg/commands/pull_request_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGetRepoInfoFromURL is a function.
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(getRepoInfoFromURL(s.repoURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *Branch
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket",
|
||||
&Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/profile-page"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket with http remote url",
|
||||
&Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/events"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on github",
|
||||
&Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on gitlab",
|
||||
&Branch{
|
||||
Name: "feature/ui",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Throws an error if git service is unsupported",
|
||||
&Branch{
|
||||
Name: "feature/divide-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "git@something.com:peter/calculator.git")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := newDummyGitCommand()
|
||||
gitCommand.OSCommand.command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
}
|
||||
}
|
||||
13
pkg/commands/stash_entry.go
Normal file
13
pkg/commands/stash_entry.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package commands
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
func (s *StashEntry) GetDisplayStrings() []string {
|
||||
return []string{s.DisplayString}
|
||||
}
|
||||
271
pkg/config/app_config.go
Normal file
271
pkg/config/app_config.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// 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
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
}
|
||||
|
||||
// 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{},
|
||||
IsNewRepo: false,
|
||||
}
|
||||
|
||||
if err := appConfig.LoadAppState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
// GetIsNewRepo returns known repo boolean
|
||||
func (c *AppConfig) GetIsNewRepo() bool {
|
||||
return c.IsNewRepo
|
||||
}
|
||||
|
||||
// SetIsNewRepo set if the current repo is known
|
||||
func (c *AppConfig) SetIsNewRepo(toSet bool) {
|
||||
c.IsNewRepo = toSet
|
||||
}
|
||||
|
||||
// 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 has 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
|
||||
scrollPastBottom: true
|
||||
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
|
||||
// }
|
||||
11
pkg/config/config_default_platform.go
Normal file
11
pkg/config/config_default_platform.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !windows,!linux
|
||||
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'open {{filename}}'
|
||||
openLinkCommand: 'open {{link}}'`)
|
||||
}
|
||||
9
pkg/config/config_linux.go
Normal file
9
pkg/config/config_linux.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
|
||||
}
|
||||
9
pkg/config/config_windows.go
Normal file
9
pkg/config/config_windows.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package main
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -15,53 +20,63 @@ import (
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
|
||||
type branchListBuilder struct{}
|
||||
|
||||
func newBranchListBuilder() *branchListBuilder {
|
||||
return &branchListBuilder{}
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
}
|
||||
|
||||
func (b *branchListBuilder) obtainCurrentBranch() 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, _ := runDirectCommand("git symbolic-ref --short HEAD")
|
||||
return Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
||||
// 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 (*branchListBuilder) obtainReflogBranches() []Branch {
|
||||
branches := make([]Branch, 0)
|
||||
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
|
||||
branchName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return &commands.Branch{Name: strings.TrimSpace(branchName)}
|
||||
}
|
||||
|
||||
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 := splitLines(rawString)
|
||||
branchLines := utils.SplitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return branches
|
||||
return uniqueByName(branches)
|
||||
}
|
||||
|
||||
func (b *branchListBuilder) obtainSafeBranches() []Branch {
|
||||
branches := make([]Branch, 0)
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
|
||||
bIter, err := r.Branches()
|
||||
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, Branch{Name: name})
|
||||
branches = append(branches, &commands.Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch {
|
||||
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)
|
||||
@@ -70,7 +85,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
||||
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
|
||||
@@ -79,15 +94,13 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
||||
return reflogBranch.Name
|
||||
}
|
||||
|
||||
func (b *branchListBuilder) build() []Branch {
|
||||
branches := make([]Branch, 0)
|
||||
// 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([]Branch{head}, reflogBranches...))
|
||||
for i, reflogBranch := range reflogBranches {
|
||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||
}
|
||||
@@ -95,11 +108,26 @@ func (b *branchListBuilder) build() []Branch {
|
||||
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
|
||||
if len(branches) == 0 || branches[0].Name != head.Name {
|
||||
branches = append([]*commands.Branch{head}, branches...)
|
||||
}
|
||||
|
||||
branches[0].Recency = " *"
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func uniqueByName(branches []Branch) []Branch {
|
||||
finalBranches := make([]Branch, 0)
|
||||
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
|
||||
@@ -115,7 +143,7 @@ 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]
|
||||
return words[0], words[1], words[len(words)-1]
|
||||
}
|
||||
|
||||
func abbreviatedTimeUnit(timeUnit string) string {
|
||||
156
pkg/git/patch_modifier.go
Normal file
156
pkg/git/patch_modifier.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
Tr *i18n.Localizer
|
||||
}
|
||||
|
||||
// NewPatchModifier builds a new branch list builder
|
||||
func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
|
||||
return &PatchModifier{
|
||||
Log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ModifyPatchForHunk takes the original patch, which may contain several hunks,
|
||||
// and removes any hunks that aren't the selected hunk
|
||||
func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) {
|
||||
// get hunk start and end
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine)
|
||||
hunkStart := hunkStarts[hunkStartIndex]
|
||||
nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine)
|
||||
var hunkEnd int
|
||||
if nextHunkStartIndex == 0 {
|
||||
hunkEnd = len(lines) - 1
|
||||
} else {
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
headerLength, err := p.getHeaderLength(lines)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) {
|
||||
for index, line := range patchLines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
return index, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New(p.Tr.SLocalize("CantFindHunks"))
|
||||
}
|
||||
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
headerLength, err := p.getHeaderLength(lines)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output += strings.Join(hunk, "\n")
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// getHunkStart returns the line number of the hunk we're going to be modifying
|
||||
// in order to stage our line
|
||||
func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) {
|
||||
// find the hunk that we're modifying
|
||||
hunkStart := 0
|
||||
for index, line := range patchLines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
hunkStart = index
|
||||
}
|
||||
if index == lineNumber {
|
||||
return hunkStart, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
|
||||
}
|
||||
|
||||
func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
|
||||
lineChanges := 0
|
||||
// strip the hunk down to just the line we want to stage
|
||||
newHunk := []string{patchLines[hunkStart]}
|
||||
for offsetIndex, line := range patchLines[hunkStart+1:] {
|
||||
index := offsetIndex + hunkStart + 1
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
newHunk = append(newHunk, "\n")
|
||||
break
|
||||
}
|
||||
if index != lineNumber {
|
||||
// we include other removals but treat them like context
|
||||
if strings.HasPrefix(line, "-") {
|
||||
newHunk = append(newHunk, " "+line[1:])
|
||||
lineChanges += 1
|
||||
continue
|
||||
}
|
||||
// we don't include other additions
|
||||
if strings.HasPrefix(line, "+") {
|
||||
lineChanges -= 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
newHunk = append(newHunk, line)
|
||||
}
|
||||
|
||||
var err error
|
||||
newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHunk, nil
|
||||
}
|
||||
|
||||
// updatedHeader returns the hunk header with the updated line range
|
||||
// we need to update the hunk length to reflect the changes we made
|
||||
// if the hunk has three additions but we're only staging one, then
|
||||
// @@ -14,8 +14,11 @@ import (
|
||||
// becomes
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
re := regexp.MustCompile(`(\d+) @@`)
|
||||
prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
re = regexp.MustCompile(`\d+ @@`)
|
||||
newLength := strconv.Itoa(prevLength + lineChanges)
|
||||
return re.ReplaceAllString(currentHeader, newLength+" @@"), nil
|
||||
}
|
||||
89
pkg/git/patch_modifier_test.go
Normal file
89
pkg/git/patch_modifier_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newDummyLog() *logrus.Entry {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
return log.WithField("test", "test")
|
||||
}
|
||||
|
||||
func newDummyPatchModifier() *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: newDummyLog(),
|
||||
}
|
||||
}
|
||||
func TestModifyPatchForLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchFilename string
|
||||
lineNumber int
|
||||
shouldError bool
|
||||
expectedPatchFilename string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Removing one line",
|
||||
"testdata/testPatchBefore.diff",
|
||||
8,
|
||||
false,
|
||||
"testdata/testPatchAfter1.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line",
|
||||
"testdata/testPatchBefore.diff",
|
||||
10,
|
||||
false,
|
||||
"testdata/testPatchAfter2.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line in top hunk in diff with multiple hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
20,
|
||||
false,
|
||||
"testdata/testPatchAfter3.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line in top hunk in diff with multiple hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
53,
|
||||
false,
|
||||
"testdata/testPatchAfter4.diff",
|
||||
},
|
||||
{
|
||||
"adding unstaged file with a single line",
|
||||
"testdata/addedFile.diff",
|
||||
6,
|
||||
false,
|
||||
"testdata/addedFile.diff",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
p := newDummyPatchModifier()
|
||||
beforePatch, err := ioutil.ReadFile(s.patchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.patchFilename)
|
||||
}
|
||||
afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
|
||||
if s.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
expected, err := ioutil.ReadFile(s.expectedPatchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.expectedPatchFilename)
|
||||
}
|
||||
assert.Equal(t, string(expected), afterPatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
36
pkg/git/patch_parser.go
Normal file
36
pkg/git/patch_parser.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchParser struct {
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry) (*PatchParser, error) {
|
||||
return &PatchParser{
|
||||
Log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastHeader := false
|
||||
for index, line := range lines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
pastHeader = true
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
}
|
||||
if pastHeader && (strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+")) {
|
||||
stageableLines = append(stageableLines, index)
|
||||
}
|
||||
}
|
||||
p.Log.WithField("staging", "staging").Info(stageableLines)
|
||||
return hunkStarts, stageableLines, nil
|
||||
}
|
||||
65
pkg/git/patch_parser_test.go
Normal file
65
pkg/git/patch_parser_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newDummyPatchParser() *PatchParser {
|
||||
return &PatchParser{
|
||||
Log: newDummyLog(),
|
||||
}
|
||||
}
|
||||
func TestParsePatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchFilename string
|
||||
shouldError bool
|
||||
expectedStageableLines []int
|
||||
expectedHunkStarts []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Diff with one hunk",
|
||||
"testdata/testPatchBefore.diff",
|
||||
false,
|
||||
[]int{8, 9, 10, 11},
|
||||
[]int{4},
|
||||
},
|
||||
{
|
||||
"Diff with two hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
false,
|
||||
[]int{8, 9, 10, 11, 12, 13, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 34, 35, 36, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53},
|
||||
[]int{4, 41},
|
||||
},
|
||||
{
|
||||
"Unstaged file",
|
||||
"testdata/addedFile.diff",
|
||||
false,
|
||||
[]int{6},
|
||||
[]int{5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
p := newDummyPatchParser()
|
||||
beforePatch, err := ioutil.ReadFile(s.patchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.patchFilename)
|
||||
}
|
||||
hunkStarts, stageableLines, err := p.ParsePatch(string(beforePatch))
|
||||
if s.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedStageableLines, stageableLines)
|
||||
assert.Equal(t, s.expectedHunkStarts, hunkStarts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
pkg/git/testdata/addedFile.diff
vendored
Normal file
7
pkg/git/testdata/addedFile.diff
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
diff --git a/blah b/blah
|
||||
new file mode 100644
|
||||
index 0000000..907b308
|
||||
--- /dev/null
|
||||
+++ b/blah
|
||||
@@ -0,0 +1 @@
|
||||
+blah
|
||||
13
pkg/git/testdata/testPatchAfter1.diff
vendored
Normal file
13
pkg/git/testdata/testPatchAfter1.diff
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
|
||||
// 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
|
||||
14
pkg/git/testdata/testPatchAfter2.diff
vendored
Normal file
14
pkg/git/testdata/testPatchAfter2.diff
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
|
||||
// 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.
|
||||
+// test 2 - if I remove this, I decrement the end counter
|
||||
// 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
|
||||
25
pkg/git/testdata/testPatchAfter3.diff
vendored
Normal file
25
pkg/git/testdata/testPatchAfter3.diff
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
headerLength := 4
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
+func getHeaderLength(patchLines []string) (int, error) {
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
headerLength := 4
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
|
||||
19
pkg/git/testdata/testPatchAfter4.diff
vendored
Normal file
19
pkg/git/testdata/testPatchAfter4.diff
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
|
||||
matches := re.FindStringSubmatch(currentHeader)
|
||||
if len(matches) < 2 {
|
||||
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
|
||||
matches = re.FindStringSubmatch(currentHeader)
|
||||
}
|
||||
prevLengthString := matches[1]
|
||||
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
15
pkg/git/testdata/testPatchBefore.diff
vendored
Normal file
15
pkg/git/testdata/testPatchBefore.diff
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
// 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.
|
||||
+// test 2 - if I remove this, I decrement the end counter
|
||||
+// test
|
||||
// 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
|
||||
57
pkg/git/testdata/testPatchBefore2.diff
vendored
Normal file
57
pkg/git/testdata/testPatchBefore2.diff
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
- headerLength := 4
|
||||
+ headerLength, err := getHeaderLength(lines)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
+
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
+func getHeaderLength(patchLines []string) (int, error) {
|
||||
+ for index, line := range patchLines {
|
||||
+ if strings.HasPrefix(line, "@@") {
|
||||
+ return index, nil
|
||||
+ }
|
||||
+ }
|
||||
+ return 0, errors.New("Could not find any hunks in this patch")
|
||||
+}
|
||||
+
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
- headerLength := 4
|
||||
+ headerLength, err := getHeaderLength(lines)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
|
||||
- matches := re.FindStringSubmatch(currentHeader)
|
||||
- if len(matches) < 2 {
|
||||
- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
|
||||
- matches = re.FindStringSubmatch(currentHeader)
|
||||
- }
|
||||
- prevLengthString := matches[1]
|
||||
+ re := regexp.MustCompile(`(\d+) @@`)
|
||||
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
269
pkg/gui/branches_panel.go
Normal file
269
pkg/gui/branches_panel.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedBranch() *commands.Branch {
|
||||
selectedLine := gui.State.Panels.Branches.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Branches[selectedLine]
|
||||
}
|
||||
|
||||
// may want to standardise how these select methods work
|
||||
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
// 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"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = gui.RenderSelectedBranchUpstreamDifferences()
|
||||
}()
|
||||
go func() {
|
||||
graph, err := gui.GitCommand.GetBranchGraph(branch.Name)
|
||||
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
|
||||
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
|
||||
}
|
||||
_ = gui.renderString(g, "main", graph)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
|
||||
// here we tell the selected branch that it is selected.
|
||||
// this is necessary for showing stats on a branch that is selected, because
|
||||
// the displaystring function doesn't have access to gui state to tell if it's selected
|
||||
for i, branch := range gui.State.Branches {
|
||||
branch.Selected = i == gui.State.Panels.Branches.SelectedLine
|
||||
}
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
|
||||
return gui.renderListPanel(gui.getBranchesView(gui.g), gui.State.Branches)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
|
||||
if err := gui.resetOrigin(gui.getBranchesView(gui.g)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshStatus(g)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Branches
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
|
||||
return gui.handleBranchSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Branches
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
|
||||
|
||||
return gui.handleBranchSelect(gui.g, v)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Branches.SelectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
if gui.State.Panels.Branches.SelectedLine == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||
if err := gui.createErrorPanel(g, err.Error()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("FetchWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend, err := gui.fetch(g, v, true)
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
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 {
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
}
|
||||
checkedOutBranch := gui.State.Branches[0]
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
}
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, force)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
|
||||
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 {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(g, errMessage)
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
checkedOutBranch := gui.State.Branches[0]
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
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) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "" {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "?" {
|
||||
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with no upstream")
|
||||
}
|
||||
if branch.Pushables != "0" {
|
||||
return gui.createErrorPanel(gui.g, "Cannot fast-forward a branch with commits to push")
|
||||
}
|
||||
upstream := "origin" // hardcoding for now
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"Fetching",
|
||||
Teml{
|
||||
"from": fmt.Sprintf("%s/%s", upstream, branch.Name),
|
||||
"to": branch.Name,
|
||||
},
|
||||
)
|
||||
go func() {
|
||||
_ = gui.createMessagePanel(gui.g, v, "", message)
|
||||
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(gui.g)
|
||||
_ = gui.RenderSelectedBranchUpstreamDifferences()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
92
pkg/gui/commit_message_panel.go
Normal file
92
pkg/gui/commit_message_panel.go
Normal file
@@ -0,0 +1,92 @@
|
||||
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(message, false)
|
||||
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
|
||||
}
|
||||
v.Clear()
|
||||
_ = v.SetCursor(0, 0)
|
||||
_ = v.SetOrigin(0, 0)
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
_ = gui.switchFocus(g, v, gui.getFilesView(g))
|
||||
return gui.refreshSidePanels(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 {
|
||||
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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) + " "
|
||||
}
|
||||
|
||||
// RenderCommitLength is a function.
|
||||
func (gui *Gui) RenderCommitLength() {
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
|
||||
return
|
||||
}
|
||||
v := gui.getCommitMessageView(gui.g)
|
||||
v.Subtitle = gui.getBufferLength(v)
|
||||
}
|
||||
186
pkg/gui/commits_panel.go
Normal file
186
pkg/gui/commits_panel.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
|
||||
selectedLine := gui.State.Panels.Commits.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Commits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil {
|
||||
return err
|
||||
}
|
||||
commitText, err := gui.GitCommand.Show(commit.Sha)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
commits, err := gui.GitCommand.GetCommits()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
|
||||
|
||||
list, err := utils.RenderList(gui.State.Commits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := gui.getCommitsView(gui.g)
|
||||
v.Clear()
|
||||
fmt.Fprint(v, list)
|
||||
|
||||
gui.refreshStatus(g)
|
||||
if v == g.CurrentView() {
|
||||
gui.handleCommitSelect(g, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Commits
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
|
||||
|
||||
return gui.handleCommitSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Commits
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
|
||||
|
||||
return gui.handleCommitSelect(gui.g, v)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
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 := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")))
|
||||
}
|
||||
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)
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
return gui.handleCommitSelect(g, commitView)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Commits.SelectedLine != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
|
||||
}
|
||||
if len(gui.State.Commits) <= 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
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 := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
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.State.Panels.Commits.SelectedLine != 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)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Panels.Commits.SelectedLine != 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
|
||||
}
|
||||
170
pkg/gui/confirmation_panel.go
Normal file
170
pkg/gui/confirmation_panel.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// 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"
|
||||
"time"
|
||||
|
||||
"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 {
|
||||
return nil // if it's already been closed we can just return
|
||||
}
|
||||
if err := gui.returnFocus(g, view); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
g.DeleteKeybindings("confirmation")
|
||||
return g.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||
lines := strings.Split(message, "\n")
|
||||
lineCount := 0
|
||||
// if we need to wrap, calculate height to fit content within view's width
|
||||
if wrap {
|
||||
for _, line := range lines {
|
||||
lineCount += len(line)/width + 1
|
||||
}
|
||||
} else {
|
||||
lineCount = len(lines)
|
||||
}
|
||||
return lineCount
|
||||
}
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
|
||||
width, height := g.Size()
|
||||
panelWidth := width / 2
|
||||
panelHeight := gui.getMessageHeight(wrap, 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, true, 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.Wrap = true
|
||||
confirmationView.FgColor = gocui.ColorWhite
|
||||
}
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
confirmationView.Clear()
|
||||
return gui.switchFocus(gui.g, currentView, confirmationView)
|
||||
})
|
||||
return confirmationView, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onNewPopupPanel() {
|
||||
_, _ = gui.g.SetViewOnBottom("commitMessage")
|
||||
_, _ = gui.g.SetViewOnBottom("credentials")
|
||||
}
|
||||
|
||||
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
|
||||
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)
|
||||
}
|
||||
|
||||
// createSpecificErrorPanel allows you to create an error popup, specifying the
|
||||
// view to be focused when the user closes the popup, and a boolean specifying
|
||||
// whether we will log the error. If the message may include a user password,
|
||||
// this function is to be used over the more generic createErrorPanel, with
|
||||
// willLog set to false
|
||||
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
|
||||
if willLog {
|
||||
go func() {
|
||||
// when reporting is switched on this log call sometimes introduces
|
||||
// a delay on the error panel popping up. Here I'm adding a second wait
|
||||
// so that the error is logged while the user is reading the error message
|
||||
time.Sleep(time.Second)
|
||||
gui.Log.Error(message)
|
||||
}()
|
||||
}
|
||||
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
|
||||
}
|
||||
104
pkg/gui/credentials_panel.go
Normal file
104
pkg/gui/credentials_panel.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type credentials chan string
|
||||
|
||||
// waitForPassUname wait for a username or password input from the credentials popup
|
||||
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
|
||||
gui.credentials = make(chan string)
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
credentialsView, _ := g.View("credentials")
|
||||
if passOrUname == "username" {
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
credentialsView.Mask = 0
|
||||
} else {
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
|
||||
credentialsView.Mask = '*'
|
||||
}
|
||||
err := gui.switchFocus(g, currentView, credentialsView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.RenderCommitLength()
|
||||
return nil
|
||||
})
|
||||
|
||||
// wait for username/passwords input
|
||||
userInput := <-gui.credentials
|
||||
return userInput + "\n"
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
gui.credentials <- message
|
||||
err := gui.refreshFiles(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Clear()
|
||||
err = v.SetCursor(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = g.SetViewOnBottom("credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextView, err := gui.g.View("confirmation")
|
||||
if err != nil {
|
||||
nextView = gui.getFilesView(g)
|
||||
}
|
||||
err = gui.switchFocus(g, nil, nextView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshCommits(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
|
||||
_, err := g.SetViewOnBottom("credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.credentials <- ""
|
||||
return gui.switchFocus(g, nil, gui.getFilesView(g))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := g.SetViewOnTop("credentials"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
},
|
||||
)
|
||||
return gui.renderString(g, "options", message)
|
||||
}
|
||||
|
||||
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
|
||||
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
|
||||
if popupOpened {
|
||||
_, _ = gui.g.SetViewOnBottom("credentials")
|
||||
}
|
||||
if cmdErr != nil {
|
||||
errMessage := cmdErr.Error()
|
||||
if strings.Contains(errMessage, "Invalid username or password") {
|
||||
errMessage = gui.Tr.SLocalize("PassUnameWrong")
|
||||
}
|
||||
// we are not logging this error because it may contain a password
|
||||
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(gui.g), false)
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(g)
|
||||
_ = gui.refreshSidePanels(g)
|
||||
}
|
||||
}
|
||||
472
pkg/gui/files_panel.go
Normal file
472
pkg/gui/files_panel.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return &commands.File{}, gui.Errors.ErrNoFiles
|
||||
}
|
||||
|
||||
return gui.State.Files[selectedLine], nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
|
||||
}
|
||||
|
||||
if file.HasMergeConflicts {
|
||||
return gui.refreshMergePanel(g)
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := gui.GitCommand.Diff(file, false)
|
||||
if alreadySelected {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getMainView(gui.g), content)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return gui.renderString(g, "main", content)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
|
||||
selectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
|
||||
filesView, err := g.View("files")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.refreshStateFiles()
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
|
||||
filesView.Clear()
|
||||
list, err := utils.RenderList(gui.State.Files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(filesView, list)
|
||||
|
||||
if filesView == g.CurrentView() {
|
||||
newSelectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
alreadySelected := newSelectedFile.Name == selectedFile.Name
|
||||
return gui.handleFileSelect(g, filesView, alreadySelected)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Files
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
|
||||
|
||||
return gui.handleFileSelect(gui.g, v, false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Files
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
|
||||
|
||||
return gui.handleFileSelect(gui.g, v, false)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
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) handleSwitchToStagingPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
stagingView, err := g.View("staging")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !file.HasUnstagedChanges {
|
||||
gui.Log.WithField("staging", "staging").Info("making error panel")
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
}
|
||||
if err := gui.switchFocus(g, v, stagingView); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshStagingPanel()
|
||||
}
|
||||
|
||||
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, true)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
return gui.refreshFiles(g)
|
||||
}
|
||||
|
||||
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) 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) 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
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
}
|
||||
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
|
||||
question := gui.Tr.SLocalize("SureToAmend")
|
||||
|
||||
if len(gui.State.Commits) == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
|
||||
lastCommitMsg := gui.State.Commits[0].Name
|
||||
_, err := gui.GitCommand.Commit(lastCommitMsg, true)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(g)
|
||||
}, 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.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.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) 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) pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.createMessagePanel(gui.g, v, "", gui.Tr.SLocalize("PullWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend := false
|
||||
err := gui.GitCommand.Pull(func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(g, v, passOrUname)
|
||||
})
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
if err := gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend := false
|
||||
branchName := gui.State.Branches[0].Name
|
||||
err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(g, v, passOrUname)
|
||||
})
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
}()
|
||||
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.GetCurrentBranchUpstreamDifferenceCount()
|
||||
if pullables == "?" || pullables == "0" {
|
||||
return gui.pushWithForceFlag(g, 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(g, 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) handleResetAndClean(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.ResetAndClean(); 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
|
||||
}
|
||||
587
pkg/gui/gui.go
Normal file
587
pkg/gui/gui.go
Normal file
@@ -0,0 +1,587 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// "strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"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
|
||||
ErrSwitchRepo 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")),
|
||||
ErrSwitchRepo: errors.New("switching repo"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
credentials credentials
|
||||
waitForIntro sync.WaitGroup
|
||||
}
|
||||
|
||||
// for now the staging panel state, unlike the other panel states, is going to be
|
||||
// non-mutative, so that we don't accidentally end up
|
||||
// with mismatches of data. We might change this in the future
|
||||
type stagingPanelState struct {
|
||||
SelectedLine int
|
||||
StageableLines []int
|
||||
HunkStarts []int
|
||||
Diff string
|
||||
}
|
||||
|
||||
type filePanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type branchPanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type commitPanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type stashPanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type menuPanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type panelStates struct {
|
||||
Files *filePanelState
|
||||
Staging *stagingPanelState
|
||||
Branches *branchPanelState
|
||||
Commits *commitPanelState
|
||||
Stash *stashPanelState
|
||||
Menu *menuPanelState
|
||||
}
|
||||
|
||||
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
|
||||
Panels *panelStates
|
||||
}
|
||||
|
||||
// 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,
|
||||
Panels: &panelStates{
|
||||
Files: &filePanelState{SelectedLine: -1},
|
||||
Branches: &branchPanelState{SelectedLine: 0},
|
||||
Commits: &commitPanelState{SelectedLine: -1},
|
||||
Stash: &stashPanelState{SelectedLine: -1},
|
||||
Menu: &menuPanelState{SelectedLine: 0},
|
||||
},
|
||||
}
|
||||
|
||||
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()
|
||||
y := oy
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
|
||||
_, sy := mainView.Size()
|
||||
y += sy
|
||||
}
|
||||
if y < 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
|
||||
g.SetViewOnTop("limit")
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
_, _ = g.SetViewOnBottom("limit")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
v, err = g.SetView("staging", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("StagingTitle")
|
||||
v.Highlight = true
|
||||
v.FgColor = gocui.ColorWhite
|
||||
if _, err := g.SetViewOnBottom("staging"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
if err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
|
||||
branchesView.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 check, _ := g.View("credentials"); check == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if credentialsView, err := g.SetView("credentials", 0, 0, width/2, height/2, 0); err != nil {
|
||||
if err != gocui.ErrUnknownView {
|
||||
return err
|
||||
}
|
||||
_, err := g.SetViewOnBottom("credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
credentialsView.FgColor = gocui.ColorWhite
|
||||
credentialsView.Editable = true
|
||||
credentialsView.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)
|
||||
if err := gui.updateRecentRepoList(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listViews := map[*gocui.View]int{
|
||||
filesView: gui.State.Panels.Files.SelectedLine,
|
||||
branchesView: gui.State.Panels.Branches.SelectedLine,
|
||||
}
|
||||
for view, selectedLine := range listViews {
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
if err := gui.focusPoint(0, selectedLine, view); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// here is a good place log some stuff
|
||||
// if you download humanlog and do tail -f development.log | humanlog
|
||||
// this will let you see these branches as prettified json
|
||||
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
||||
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 {
|
||||
gui.waitForIntro.Done()
|
||||
return gui.Config.WriteToUserConfig("reporting", "on")
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.waitForIntro.Done()
|
||||
return gui.Config.WriteToUserConfig("reporting", "off")
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (unamePassOpend bool, err error) {
|
||||
unamePassOpend = false
|
||||
err = gui.GitCommand.Fetch(func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(gui.g, v, passOrUname)
|
||||
}, canAskForCredentials)
|
||||
|
||||
if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong")))
|
||||
close := func(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
_ = gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
|
||||
}
|
||||
|
||||
gui.refreshStatus(g)
|
||||
return unamePassOpend, err
|
||||
}
|
||||
|
||||
func (gui *Gui) updateLoader(g *gocui.Gui) error {
|
||||
gui.g.Update(func(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.setViewContent(g, view, staticContent+" "+utils.Loader()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
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() error {
|
||||
return gui.renderOptionsMap(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
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
|
||||
gui.waitForIntro.Add(2)
|
||||
} else {
|
||||
gui.waitForIntro.Add(1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
gui.waitForIntro.Wait()
|
||||
isNew := gui.Config.GetIsNewRepo()
|
||||
if !isNew {
|
||||
time.After(60 * time.Second)
|
||||
}
|
||||
_, err := gui.fetch(g, g.CurrentView(), false)
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
|
||||
_ = gui.createConfirmationPanel(g, g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
|
||||
} else {
|
||||
gui.goEvery(g, time.Second*60, func(g *gocui.Gui) error {
|
||||
_, err := gui.fetch(g, g.CurrentView(), false)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}()
|
||||
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.ErrSwitchRepo {
|
||||
continue
|
||||
} 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
|
||||
}
|
||||
524
pkg/gui/keybindings.go
Normal file
524
pkg/gui/keybindings.go
Normal file
@@ -0,0 +1,524 @@
|
||||
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
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
func (b *Binding) GetDisplayStrings() []string {
|
||||
return []string{b.GetKey(), b.Description}
|
||||
}
|
||||
|
||||
// GetKey is a function.
|
||||
func (b *Binding) GetKey() string {
|
||||
key := 0
|
||||
|
||||
switch b.Key.(type) {
|
||||
case rune:
|
||||
key = int(b.Key.(rune))
|
||||
case gocui.Key:
|
||||
key = int(b.Key.(gocui.Key))
|
||||
}
|
||||
|
||||
// special keys
|
||||
switch key {
|
||||
case 27:
|
||||
return "esc"
|
||||
case 13:
|
||||
return "enter"
|
||||
case 32:
|
||||
return "space"
|
||||
}
|
||||
|
||||
return string(key)
|
||||
}
|
||||
|
||||
// GetKeybindings is a function.
|
||||
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.handleCreateOptionsMenu,
|
||||
}, {
|
||||
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: "status",
|
||||
Key: 's',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateRecentReposMenu,
|
||||
Description: gui.Tr.SLocalize("SwitchRepo"),
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Key: 'c',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitPress,
|
||||
Description: gui.Tr.SLocalize("CommitChanges"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'A',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleAmendCommitPress,
|
||||
Description: gui.Tr.SLocalize("AmendLastCommit"),
|
||||
}, {
|
||||
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,
|
||||
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: 'M',
|
||||
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.handleResetAndClean,
|
||||
Description: gui.Tr.SLocalize("resetHard"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSwitchToStagingPanel,
|
||||
Description: gui.Tr.SLocalize("StageLines"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'f',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleGitFetch,
|
||||
Description: gui.Tr.SLocalize("fetch"),
|
||||
}, {
|
||||
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,
|
||||
Description: gui.Tr.SLocalize("checkout"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreatePullRequestPress,
|
||||
Description: gui.Tr.SLocalize("createPullRequest"),
|
||||
}, {
|
||||
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: 'm',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMerge,
|
||||
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'f',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFastForward,
|
||||
Description: gui.Tr.SLocalize("FastForward"),
|
||||
}, {
|
||||
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,
|
||||
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: "credentials",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSubmitCredential,
|
||||
}, {
|
||||
ViewName: "credentials",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCloseCredentialsView,
|
||||
}, {
|
||||
ViewName: "menu",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenuClose,
|
||||
}, {
|
||||
ViewName: "menu",
|
||||
Key: 'q',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMenuClose,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingEscape,
|
||||
Description: gui.Tr.SLocalize("EscapeStaging"),
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevLine,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextLine,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevLine,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextLine,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevHunk,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextHunk,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevHunk,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextHunk,
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageLine,
|
||||
Description: gui.Tr.SLocalize("StageLine"),
|
||||
}, {
|
||||
ViewName: "staging",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageHunk,
|
||||
Description: gui.Tr.SLocalize("StageHunk"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, viewName := range []string{"status", "branches", "files", "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: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
}...)
|
||||
}
|
||||
|
||||
listPanelMap := map[string]struct {
|
||||
prevLine func(*gocui.Gui, *gocui.View) error
|
||||
nextLine func(*gocui.Gui, *gocui.View) error
|
||||
}{
|
||||
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine},
|
||||
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine},
|
||||
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine},
|
||||
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine},
|
||||
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine},
|
||||
}
|
||||
|
||||
for viewName, functions := range listPanelMap {
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.prevLine},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
|
||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
|
||||
}...)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
84
pkg/gui/menu_panel.go
Normal file
84
pkg/gui/menu_panel.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Menu
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
|
||||
|
||||
return gui.handleMenuSelect(g, v)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) renderMenuOptions() error {
|
||||
optionsMap := map[string]string{
|
||||
"esc/q": gui.Tr.SLocalize("close"),
|
||||
"↑ ↓": gui.Tr.SLocalize("navigate"),
|
||||
"space": gui.Tr.SLocalize("execute"),
|
||||
}
|
||||
return gui.renderOptionsMap(optionsMap)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
|
||||
return err
|
||||
}
|
||||
err := g.DeleteView("menu")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.returnFocus(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
|
||||
list, err := utils.RenderList(items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
||||
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
|
||||
menuView.FgColor = gocui.ColorWhite
|
||||
menuView.Clear()
|
||||
fmt.Fprint(menuView, list)
|
||||
gui.State.Panels.Menu.SelectedLine = 0
|
||||
|
||||
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLine
|
||||
return handlePress(selectedLine)
|
||||
}
|
||||
|
||||
if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if _, err := g.SetViewOnTop("menu"); err != nil {
|
||||
return err
|
||||
}
|
||||
currentView := gui.g.CurrentView()
|
||||
return gui.switchFocus(gui.g, currentView, menuView)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
260
pkg/gui/merge_panel.go
Normal file
260
pkg/gui/merge_panel.go
Normal file
@@ -0,0 +1,260 @@
|
||||
// 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"
|
||||
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() error {
|
||||
return gui.renderOptionsMap(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)
|
||||
}
|
||||
51
pkg/gui/options_menu_panel.go
Normal file
51
pkg/gui/options_menu_panel.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||
var (
|
||||
bindingsGlobal, bindingsPanel []*Binding
|
||||
)
|
||||
|
||||
bindings := gui.GetKeybindings()
|
||||
|
||||
for _, binding := range bindings {
|
||||
if binding.GetKey() != "" && binding.Description != "" {
|
||||
switch binding.ViewName {
|
||||
case "":
|
||||
bindingsGlobal = append(bindingsGlobal, binding)
|
||||
case v.Name():
|
||||
bindingsPanel = append(bindingsPanel, binding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append dummy element to have a separator between
|
||||
// panel and global keybindings
|
||||
bindingsPanel = append(bindingsPanel, &Binding{})
|
||||
return append(bindingsPanel, bindingsGlobal...)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
bindings := gui.getBindings(v)
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
if bindings[index].Key == nil {
|
||||
return nil
|
||||
}
|
||||
if index >= len(bindings) {
|
||||
return errors.New("Index is greater than size of bindings")
|
||||
}
|
||||
err := gui.handleMenuClose(g, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bindings[index].Handler(g, v)
|
||||
}
|
||||
|
||||
return gui.createMenu(bindings, handleMenuPress)
|
||||
}
|
||||
76
pkg/gui/recent_repos_panel.go
Normal file
76
pkg/gui/recent_repos_panel.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type recentRepo struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the path from a recent repo.
|
||||
func (r *recentRepo) GetDisplayStrings() []string {
|
||||
yellow := color.New(color.FgMagenta)
|
||||
base := filepath.Base(r.path)
|
||||
path := yellow.Sprint(r.path)
|
||||
return []string{base, path}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
recentRepoPaths := gui.Config.GetAppState().RecentRepos
|
||||
reposCount := utils.Min(len(recentRepoPaths), 20)
|
||||
// we won't show the current repo hence the -1
|
||||
recentRepos := make([]*recentRepo, reposCount-1)
|
||||
for i, path := range recentRepoPaths[1:reposCount] {
|
||||
recentRepos[i] = &recentRepo{path: path}
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
repo := recentRepos[index]
|
||||
if err := os.Chdir(repo.path); err != nil {
|
||||
return err
|
||||
}
|
||||
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.GitCommand = newGitCommand
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
}
|
||||
|
||||
return gui.createMenu(recentRepos, handleMenuPress)
|
||||
}
|
||||
|
||||
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
|
||||
// so that we can open the same repo via the 'recent repos' menu
|
||||
func (gui *Gui) updateRecentRepoList() error {
|
||||
recentRepos := gui.Config.GetAppState().RecentRepos
|
||||
currentRepo, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
|
||||
gui.Config.SetIsNewRepo(known)
|
||||
gui.Config.GetAppState().RecentRepos = recentRepos
|
||||
return gui.Config.SaveAppState()
|
||||
}
|
||||
|
||||
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet
|
||||
func newRecentReposList(recentRepos []string, currentRepo string) (bool, []string) {
|
||||
isNew := true
|
||||
newRepos := []string{currentRepo}
|
||||
for _, repo := range recentRepos {
|
||||
if repo != currentRepo {
|
||||
newRepos = append(newRepos, repo)
|
||||
} else {
|
||||
isNew = false
|
||||
}
|
||||
}
|
||||
return isNew, newRepos
|
||||
}
|
||||
219
pkg/gui/staging_panel.go
Normal file
219
pkg/gui/staging_panel.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStagingPanel() error {
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
if !file.HasUnstagedChanges {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
|
||||
diff := gui.GitCommand.Diff(file, true)
|
||||
colorDiff := gui.GitCommand.Diff(file, false)
|
||||
|
||||
if len(diff) < 2 {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
// parse the diff and store the line numbers of hunks and stageable lines
|
||||
// TODO: maybe instantiate this at application start
|
||||
p, err := git.NewPatchParser(gui.Log)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
hunkStarts, stageableLines, err := p.ParsePatch(diff)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var selectedLine int
|
||||
if gui.State.Panels.Staging != nil {
|
||||
end := len(stageableLines) - 1
|
||||
if end < gui.State.Panels.Staging.SelectedLine {
|
||||
selectedLine = end
|
||||
} else {
|
||||
selectedLine = gui.State.Panels.Staging.SelectedLine
|
||||
}
|
||||
} else {
|
||||
selectedLine = 0
|
||||
}
|
||||
|
||||
gui.State.Panels.Staging = &stagingPanelState{
|
||||
StageableLines: stageableLines,
|
||||
HunkStarts: hunkStarts,
|
||||
SelectedLine: selectedLine,
|
||||
Diff: diff,
|
||||
}
|
||||
|
||||
if len(stageableLines) == 0 {
|
||||
return errors.New("No lines to stage")
|
||||
}
|
||||
|
||||
if err := gui.focusLineAndHunk(); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(gui.g, "staging", colorDiff)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
if _, err := gui.g.SetViewOnBottom("staging"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.Staging = nil
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getFilesView(gui.g))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleHunk(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleHunk(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCycleHunk(prev bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
lineNumbers := state.StageableLines
|
||||
currentLine := lineNumbers[state.SelectedLine]
|
||||
currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
|
||||
var newHunkIndex int
|
||||
if prev {
|
||||
if currentHunkIndex == 0 {
|
||||
newHunkIndex = len(state.HunkStarts) - 1
|
||||
} else {
|
||||
newHunkIndex = currentHunkIndex - 1
|
||||
}
|
||||
} else {
|
||||
if currentHunkIndex == len(state.HunkStarts)-1 {
|
||||
newHunkIndex = 0
|
||||
} else {
|
||||
newHunkIndex = currentHunkIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
|
||||
|
||||
return gui.focusLineAndHunk()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCycleLine(prev bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
lineNumbers := state.StageableLines
|
||||
currentLine := lineNumbers[state.SelectedLine]
|
||||
var newIndex int
|
||||
if prev {
|
||||
newIndex = utils.PrevIndex(lineNumbers, currentLine)
|
||||
} else {
|
||||
newIndex = utils.NextIndex(lineNumbers, currentLine)
|
||||
}
|
||||
state.SelectedLine = newIndex
|
||||
|
||||
return gui.focusLineAndHunk()
|
||||
}
|
||||
|
||||
// focusLineAndHunk works out the best focus for the staging panel given the
|
||||
// selected line and size of the hunk
|
||||
func (gui *Gui) focusLineAndHunk() error {
|
||||
stagingView := gui.getStagingView(gui.g)
|
||||
state := gui.State.Panels.Staging
|
||||
|
||||
lineNumber := state.StageableLines[state.SelectedLine]
|
||||
|
||||
// we want the bottom line of the view buffer to ideally be the bottom line
|
||||
// of the hunk, but if the hunk is too big we'll just go three lines beyond
|
||||
// the currently selected line so that the user can see the context
|
||||
var bottomLine int
|
||||
nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
|
||||
if nextHunkStartIndex == 0 {
|
||||
// for now linesHeight is an efficient means of getting the number of lines
|
||||
// in the patch. However if we introduce word wrap we'll need to update this
|
||||
bottomLine = stagingView.LinesHeight() - 1
|
||||
} else {
|
||||
bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
|
||||
}
|
||||
|
||||
hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
|
||||
hunkStart := state.HunkStarts[hunkStartIndex]
|
||||
// if it's the first hunk we'll also show the diff header
|
||||
if hunkStartIndex == 0 {
|
||||
hunkStart = 0
|
||||
}
|
||||
|
||||
_, height := stagingView.Size()
|
||||
// if this hunk is too big, we will just ensure that the user can at least
|
||||
// see three lines of context below the cursor
|
||||
if bottomLine-hunkStart > height {
|
||||
bottomLine = lineNumber + 3
|
||||
}
|
||||
|
||||
return gui.generalFocusLine(lineNumber, bottomLine, stagingView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStageLineOrHunk(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStageLineOrHunk(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
p, err := git.NewPatchModifier(gui.Log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentLine := state.StageableLines[state.SelectedLine]
|
||||
var patch string
|
||||
if hunk {
|
||||
patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
|
||||
} else {
|
||||
patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// for logging purposes
|
||||
// ioutil.WriteFile("patch.diff", []byte(patch), 0600)
|
||||
|
||||
// apply the patch then refresh this panel
|
||||
// create a new temp file with the patch, then call git apply with that patch
|
||||
_, err = gui.GitCommand.ApplyPatch(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
120
pkg/gui/stash_panel.go
Normal file
120
pkg/gui/stash_panel.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||
selectedLine := gui.State.Panels.Stash.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.StashEntries[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, v); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
// doing this asynchronously cos it can take time
|
||||
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
|
||||
_ = gui.renderString(g, "main", diff)
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
|
||||
|
||||
list, err := utils.RenderList(gui.State.StashEntries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := gui.getStashView(gui.g)
|
||||
v.Clear()
|
||||
fmt.Fprint(v, list)
|
||||
|
||||
return gui.resetOrigin(v)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Stash
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
|
||||
|
||||
return gui.handleStashEntrySelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.Stash
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
|
||||
|
||||
return gui.handleStashEntrySelect(gui.g, v)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
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
|
||||
}
|
||||
86
pkg/gui/status_panel.go
Normal file
86
pkg/gui/status_panel.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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.GetCurrentBranchUpstreamDifferenceCount()
|
||||
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) 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 {
|
||||
blue := color.New(color.FgBlue)
|
||||
|
||||
dashboardString := strings.Join(
|
||||
[]string{
|
||||
lazygitTitle(),
|
||||
"Copyright (c) 2018 Jesse Duffield",
|
||||
"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://youtu.be/VDXvbHZYeKY",
|
||||
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
|
||||
blue.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
|
||||
}, "\n\n")
|
||||
|
||||
return gui.renderString(g, "main", dashboardString)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
395
pkg/gui/view_helpers.go
Normal file
395
pkg/gui/view_helpers.go
Normal file
@@ -0,0 +1,395 @@
|
||||
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 {
|
||||
if err := gui.refreshBranches(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshStashEntries(g)
|
||||
}
|
||||
|
||||
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 {
|
||||
switch v.Name() {
|
||||
case "menu":
|
||||
return gui.handleMenuSelect(g, v)
|
||||
case "status":
|
||||
return gui.handleStatusSelect(g, v)
|
||||
case "files":
|
||||
return gui.handleFileSelect(g, v, false)
|
||||
case "branches":
|
||||
return gui.handleBranchSelect(g, v)
|
||||
case "commits":
|
||||
return gui.handleCommitSelect(g, v)
|
||||
case "stash":
|
||||
return gui.handleStashEntrySelect(g, v)
|
||||
case "confirmation":
|
||||
return nil
|
||||
case "commitMessage":
|
||||
return gui.handleCommitFocused(g, v)
|
||||
case "credentials":
|
||||
return gui.handleCredentialsViewFocused(g, v)
|
||||
case "main":
|
||||
// TODO: pull this out into a 'view focused' function
|
||||
gui.refreshMergePanel(g)
|
||||
v.Highlight = false
|
||||
return nil
|
||||
case "staging":
|
||||
return nil
|
||||
// return gui.handleStagingSelect(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)
|
||||
|
||||
// second class panels should never have focus restored to them because
|
||||
// once they lose focus they are effectively 'destroyed'
|
||||
secondClassPanels := []string{"confirmation", "menu"}
|
||||
if !utils.IncludesString(secondClassPanels, oldView.Name()) {
|
||||
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
|
||||
}
|
||||
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Cursor = newView.Editable
|
||||
|
||||
if err := gui.renderPanelOptions(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.newLineFocused(g, newView)
|
||||
}
|
||||
|
||||
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) focusPoint(cx int, cy int, v *gocui.View) error {
|
||||
if cy < 0 {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
_, height := v.Size()
|
||||
ly := height - 1
|
||||
|
||||
// if line is above origin, move origin and set cursor to zero
|
||||
// if line is below origin + height, move origin and set cursor to max
|
||||
// otherwise set cursor to value - origin
|
||||
if ly > v.LinesHeight() {
|
||||
if err := v.SetCursor(cx, cy); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if cy < oy {
|
||||
if err := v.SetCursor(cx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, cy); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if cy > oy+ly {
|
||||
if err := v.SetCursor(cx, ly); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, cy-ly); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := v.SetCursor(cx, cy-oy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) cleanString(s string) string {
|
||||
output := string(bom.Clean([]byte(s)))
|
||||
return utils.NormalizeLinefeeds(output)
|
||||
}
|
||||
|
||||
func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error {
|
||||
v.Clear()
|
||||
fmt.Fprint(v, gui.cleanString(s))
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderString resets the origin of a view and sets its content
|
||||
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v, err := g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // return gracefully if view has been deleted
|
||||
}
|
||||
if err := v.SetOrigin(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.setViewContent(gui.g, v, s)
|
||||
})
|
||||
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(optionsMap map[string]string) error {
|
||||
return gui.renderString(gui.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) getStagingView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("staging")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getMainView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("main")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getStashView(g *gocui.Gui) *gocui.View {
|
||||
v, _ := g.View("stash")
|
||||
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() == "credentials" || 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, v.Wrap, 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
|
||||
}
|
||||
|
||||
// generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
|
||||
func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error {
|
||||
_, height := v.Size()
|
||||
overScroll := bottomLine - height + 1
|
||||
if overScroll < 0 {
|
||||
overScroll = 0
|
||||
}
|
||||
if err := v.SetOrigin(0, overScroll); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
|
||||
if up {
|
||||
if *line == -1 || *line == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
*line -= 1
|
||||
} else {
|
||||
if *line == -1 || *line == total-1 {
|
||||
return
|
||||
}
|
||||
|
||||
*line += 1
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshSelectedLine(line *int, total int) {
|
||||
if *line == -1 && total > 0 {
|
||||
*line = 0
|
||||
} else if total-1 < *line {
|
||||
*line = total - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
list, err := utils.RenderList(items)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
v.Clear()
|
||||
fmt.Fprint(v, list)
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderPanelOptions() error {
|
||||
currentView := gui.g.CurrentView()
|
||||
switch currentView.Name() {
|
||||
case "menu":
|
||||
return gui.renderMenuOptions()
|
||||
case "main":
|
||||
return gui.renderMergeOptions()
|
||||
default:
|
||||
return gui.renderGlobalOptions()
|
||||
}
|
||||
}
|
||||
453
pkg/i18n/dutch.go
Normal file
453
pkg/i18n/dutch.go
Normal file
@@ -0,0 +1,453 @@
|
||||
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: "CredentialsUsername",
|
||||
Other: "Gebruikersnaam",
|
||||
}, &i18n.Message{
|
||||
ID: "CredentialsPassword",
|
||||
Other: "Wachtwoord",
|
||||
}, &i18n.Message{
|
||||
ID: "PassUnameWrong",
|
||||
Other: "Wachtwoord en/of gebruikersnaam verkeert",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "Commit veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendLastCommit",
|
||||
Other: "wijzig laatste commit",
|
||||
}, &i18n.Message{
|
||||
ID: "SureToAmend",
|
||||
Other: "Weet je zeker dat je de laatste commit wilt wijzigen? U kunt het commit-bericht wijzigen vanuit het commits-paneel.",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitToAmend",
|
||||
Other: "Er is geen commits om te wijzigen.",
|
||||
}, &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: "push",
|
||||
Other: "push",
|
||||
}, &i18n.Message{
|
||||
ID: "pull",
|
||||
Other: "pull",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "bewerkingen toevoegen",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "bewerken",
|
||||
}, &i18n.Message{
|
||||
ID: "scroll",
|
||||
Other: "scroll",
|
||||
}, &i18n.Message{
|
||||
ID: "abortMerge",
|
||||
Other: "samenvoegen afbreken",
|
||||
}, &i18n.Message{
|
||||
ID: "resolveMergeConflicts",
|
||||
Other: "los merge conflicten op",
|
||||
}, &i18n.Message{
|
||||
ID: "checkout",
|
||||
Other: "uitchecken",
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "Geen bestanden veranderd",
|
||||
}, &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: "NotAFile",
|
||||
Other: "Dit is geen bestand",
|
||||
}, &i18n.Message{
|
||||
ID: "PullWait",
|
||||
Other: "Pullen...",
|
||||
}, &i18n.Message{
|
||||
ID: "PushWait",
|
||||
Other: "Pushen...",
|
||||
}, &i18n.Message{
|
||||
ID: "FetchWait",
|
||||
Other: "Fetchen...",
|
||||
}, &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` en `clean -fd` 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 verwijderd)",
|
||||
}, &i18n.Message{
|
||||
ID: "AlreadyCheckedOutBranch",
|
||||
Other: "Je hebt deze branch al uitgecheckt",
|
||||
}, &i18n.Message{
|
||||
ID: "SureForceCheckout",
|
||||
Other: "Weet je zeker dat je het uitchecken wil forceren? Al je lokale 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}} wilt 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: "samenvoegen",
|
||||
}, &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: "hernoemen",
|
||||
}, &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: "Foutmelding",
|
||||
}, &i18n.Message{
|
||||
ID: "resizingPopupPanel",
|
||||
Other: "resizen popup paneel",
|
||||
}, &i18n.Message{
|
||||
ID: "RunningSubprocess",
|
||||
Other: "subprocess lopend",
|
||||
}, &i18n.Message{
|
||||
ID: "selectHunk",
|
||||
Other: "selecteer stuk",
|
||||
}, &i18n.Message{
|
||||
ID: "navigateConflicts",
|
||||
Other: "navigeer conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "pickHunk",
|
||||
Other: "kies stuk",
|
||||
}, &i18n.Message{
|
||||
ID: "pickBothHunks",
|
||||
Other: "kies beide stukken",
|
||||
}, &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: "Jouw branch is afgeweken van de remote branch. Druk 'esc' om te annuleren, of 'enter' om geforceert te pushen.",
|
||||
}, &i18n.Message{
|
||||
ID: "checkForUpdate",
|
||||
Other: "check voor updates",
|
||||
}, &i18n.Message{
|
||||
ID: "CheckingForUpdates",
|
||||
Other: "zoeken naar updates...",
|
||||
}, &i18n.Message{
|
||||
ID: "OnLatestVersionErr",
|
||||
Other: "Je hebt al de laatste versie",
|
||||
}, &i18n.Message{
|
||||
ID: "MajorVersionErr",
|
||||
Other: "Nieuwe versie ({{.newVersion}}) is niet backwards compatibele vergeleken met de huidige versie ({{.currentVersion}})",
|
||||
}, &i18n.Message{
|
||||
ID: "CouldNotFindBinaryErr",
|
||||
Other: "Kon geen binary vinden op {{.url}}",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingTitle",
|
||||
Other: "Help lazygit te verbeteren",
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "GitconfigParseErr",
|
||||
Other: `Gogit kon je gitconfig bestand niet goed parsen door de aanwezigheid van losstaande '\' tekens. Het weghalen van deze tekens zou het probleem moeten oplossen. `,
|
||||
}, &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 and verwijderen ongevolgde bestanden`,
|
||||
}, &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?`,
|
||||
}, &i18n.Message{
|
||||
ID: "SwitchRepo",
|
||||
Other: "wissel naar een recente repo",
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Niet-ondersteunde git-service`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `maak een pull-aanvraag`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
|
||||
}, &i18n.Message{
|
||||
ID: "fetch",
|
||||
Other: `fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchTitle",
|
||||
Other: `Geen automatiese git fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchBody",
|
||||
Other: `Lazygit kan niet "git fetch" uitvoeren in een privé repository, gebruik f in het branches paneel om "git fetch" manueel uit te voeren`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLines",
|
||||
Other: `stage individuele hunks/lijnen`,
|
||||
}, &i18n.Message{
|
||||
ID: "FileStagingRequirements",
|
||||
Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagingTitle",
|
||||
Other: `Staging`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageHunk",
|
||||
Other: `stage hunk`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLine",
|
||||
Other: `stage lijn`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
Other: `ga terug naar het bestanden paneel`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
Other: `Kan geen hunks vinden in deze patch`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunk",
|
||||
Other: `Kan geen hunk vinden`,
|
||||
},
|
||||
)
|
||||
}
|
||||
467
pkg/i18n/english.go
Normal file
467
pkg/i18n/english.go
Normal file
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
|
||||
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: "CredentialsUsername",
|
||||
Other: "Username",
|
||||
}, &i18n.Message{
|
||||
ID: "CredentialsPassword",
|
||||
Other: "Password",
|
||||
}, &i18n.Message{
|
||||
ID: "PassUnameWrong",
|
||||
Other: "Password and/or username wrong",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "commit changes",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendLastCommit",
|
||||
Other: "amend last commit",
|
||||
}, &i18n.Message{
|
||||
ID: "SureToAmend",
|
||||
Other: "Are you sure you want to amend last commit? You can change commit message from commits panel.",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitToAmend",
|
||||
Other: "There's no commit to amend.",
|
||||
}, &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: "FetchWait",
|
||||
Other: "Fetching...",
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "This file has no merge conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Are you sure you want `reset --hard HEAD` and `clean -fd`? 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: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
|
||||
}, &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 and remove untracked files`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `merge into currently checked out branch`,
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Are you sure you want to quit?`,
|
||||
}, &i18n.Message{
|
||||
ID: "SwitchRepo",
|
||||
Other: `switch to a recent repo`,
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Unsupported git service`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `create pull request`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `This branch doesn't exist on remote. You need to push it to remote first.`,
|
||||
}, &i18n.Message{
|
||||
ID: "fetch",
|
||||
Other: `fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchTitle",
|
||||
Other: `No automatic git fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchBody",
|
||||
Other: `Lazygit can't use "git fetch" in a private repo; use 'f' in the files panel to run "git fetch" manually`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLines",
|
||||
Other: `stage individual hunks/lines`,
|
||||
}, &i18n.Message{
|
||||
ID: "FileStagingRequirements",
|
||||
Other: `Can only stage individual lines for tracked files with unstaged changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagingTitle",
|
||||
Other: `Staging`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageHunk",
|
||||
Other: `stage hunk`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLine",
|
||||
Other: `stage line`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
Other: `return to files panel`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
Other: `Could not find any hunks in this patch`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunk",
|
||||
Other: `Could not find hunk`,
|
||||
}, &i18n.Message{
|
||||
ID: "FastForward",
|
||||
Other: `fast-forward this branch from its upstream`,
|
||||
}, &i18n.Message{
|
||||
ID: "Fetching",
|
||||
Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...",
|
||||
},
|
||||
)
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
91
pkg/i18n/i18n_test.go
Normal file
91
pkg/i18n/i18n_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
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")
|
||||
}
|
||||
|
||||
// TestNewLocalizer is a function.
|
||||
func TestNewLocalizer(t *testing.T) {
|
||||
assert.NotNil(t, NewLocalizer(getDummyLog()))
|
||||
}
|
||||
|
||||
// TestDetectLanguage is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalizer is a function.
|
||||
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 wilt verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(setupLocalizer(getDummyLog(), s.userLang))
|
||||
}
|
||||
}
|
||||
436
pkg/i18n/polish.go
Normal file
436
pkg/i18n/polish.go
Normal file
@@ -0,0 +1,436 @@
|
||||
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: "CredentialsUsername",
|
||||
Other: "Username",
|
||||
}, &i18n.Message{
|
||||
ID: "CredentialsPassword",
|
||||
Other: "Password",
|
||||
}, &i18n.Message{
|
||||
ID: "PassUnameWrong",
|
||||
Other: "Password and/or username wrong",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitChanges",
|
||||
Other: "commituj zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendLastCommit",
|
||||
Other: "zmień ostatnie zatwierdzenie",
|
||||
}, &i18n.Message{
|
||||
ID: "SureToAmend",
|
||||
Other: "Czy na pewno chcesz zmienić ostatnie zatwierdzenie? Możesz zmienić komunikat zatwierdzenia z panelu zatwierdzeń.",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommitToAmend",
|
||||
Other: "Nie ma zobowiązania do 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: "FetchWait",
|
||||
Other: "Fetching...",
|
||||
}, &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` i `clean -fd`? 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 i usuń niepotwierdzone pliki`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `scal do obecnej gałęzi`,
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Na pewno chcesz wyjść z programu?`,
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Nieobsługiwana usługa git`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `utwórz żądanie wyciągnięcia`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
|
||||
}, &i18n.Message{
|
||||
ID: "fetch",
|
||||
Other: `fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchTitle",
|
||||
Other: `No automatic git fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchBody",
|
||||
Other: `Lazygit can't use "git fetch" in a private repo use f in the branches panel to run "git fetch" manually`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLines",
|
||||
Other: `zatwierdź pojedyncze linie`,
|
||||
}, &i18n.Message{
|
||||
ID: "FileStagingRequirements",
|
||||
Other: `Można tylko zatwierdzić pojedyncze linie dla śledzonych plików z niezatwierdzonymi zmianami`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagingTitle",
|
||||
Other: `Zatwierdzanie`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageHunk",
|
||||
Other: `zatwierdź kawałek`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLine",
|
||||
Other: `zatwierdź linię`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
Other: `wróć do panelu plików`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
Other: `Nie można znaleźć żadnych kawałków w tej łatce`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunk",
|
||||
Other: `Nie można znaleźć kawałka`,
|
||||
},
|
||||
)
|
||||
}
|
||||
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")
|
||||
}
|
||||
312
pkg/updates/updates.go
Normal file
312
pkg/updates/updates.go
Normal file
@@ -0,0 +1,312 @@
|
||||
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()
|
||||
}
|
||||
|
||||
const (
|
||||
PROJECT_URL = "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")
|
||||
|
||||
return &Updater{
|
||||
Log: contextLogger,
|
||||
Config: config,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *Updater) getLatestVersionNumber() (string, error) {
|
||||
req, err := http.NewRequest("GET", PROJECT_URL+"/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()
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
data := struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}{}
|
||||
if err := dec.Decode(&data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return data.TagName, 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",
|
||||
PROJECT_URL,
|
||||
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
|
||||
}
|
||||
243
pkg/utils/utils.go
Normal file
243
pkg/utils/utils.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"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
|
||||
}
|
||||
|
||||
// Min returns the minimum of two integers
|
||||
func Min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
type Displayable interface {
|
||||
GetDisplayStrings() []string
|
||||
}
|
||||
|
||||
// RenderList takes a slice of items, confirms they implement the Displayable
|
||||
// interface, then generates a list of their displaystrings to write to a panel's
|
||||
// buffer
|
||||
func RenderList(slice interface{}) (string, error) {
|
||||
s := reflect.ValueOf(slice)
|
||||
if s.Kind() != reflect.Slice {
|
||||
return "", errors.New("RenderList given a non-slice type")
|
||||
}
|
||||
|
||||
displayables := make([]Displayable, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
value, ok := s.Index(i).Interface().(Displayable)
|
||||
if !ok {
|
||||
return "", errors.New("item does not implement the Displayable interface")
|
||||
}
|
||||
displayables[i] = value
|
||||
}
|
||||
|
||||
return renderDisplayableList(displayables)
|
||||
}
|
||||
|
||||
// renderDisplayableList takes a list of displayable items, obtains their display
|
||||
// strings via GetDisplayStrings() and then returns a single string containing
|
||||
// each item's string representation on its own line, with appropriate horizontal
|
||||
// padding between the item's own strings
|
||||
func renderDisplayableList(items []Displayable) (string, error) {
|
||||
if len(items) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
stringArrays := getDisplayStringArrays(items)
|
||||
|
||||
if !displayArraysAligned(stringArrays) {
|
||||
return "", errors.New("Each item must return the same number of strings to display")
|
||||
}
|
||||
|
||||
padWidths := getPadWidths(stringArrays)
|
||||
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
|
||||
|
||||
return strings.Join(paddedDisplayStrings, "\n"), nil
|
||||
}
|
||||
|
||||
func getPadWidths(stringArrays [][]string) []int {
|
||||
if len(stringArrays[0]) <= 1 {
|
||||
return []int{}
|
||||
}
|
||||
padWidths := make([]int, len(stringArrays[0])-1)
|
||||
for i := range padWidths {
|
||||
for _, strings := range stringArrays {
|
||||
if len(strings[i]) > padWidths[i] {
|
||||
padWidths[i] = len(strings[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return padWidths
|
||||
}
|
||||
|
||||
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
|
||||
paddedDisplayStrings := make([]string, len(stringArrays))
|
||||
for i, stringArray := range stringArrays {
|
||||
if len(stringArray) == 0 {
|
||||
continue
|
||||
}
|
||||
for j, padWidth := range padWidths {
|
||||
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
|
||||
}
|
||||
paddedDisplayStrings[i] += stringArray[len(padWidths)]
|
||||
}
|
||||
return paddedDisplayStrings
|
||||
}
|
||||
|
||||
// displayArraysAligned returns true if every string array returned from our
|
||||
// list of displayables has the same length
|
||||
func displayArraysAligned(stringArrays [][]string) bool {
|
||||
for _, strings := range stringArrays {
|
||||
if len(strings) != len(stringArrays[0]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getDisplayStringArrays(displayables []Displayable) [][]string {
|
||||
stringArrays := make([][]string, len(displayables))
|
||||
for i, item := range displayables {
|
||||
stringArrays[i] = item.GetDisplayStrings()
|
||||
}
|
||||
return stringArrays
|
||||
}
|
||||
|
||||
// IncludesString if the list contains the string
|
||||
func IncludesString(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextIndex returns the index of the element that comes after the given number
|
||||
func NextIndex(numbers []int, currentNumber int) int {
|
||||
for index, number := range numbers {
|
||||
if number > currentNumber {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
|
||||
func PrevIndex(numbers []int, currentNumber int) int {
|
||||
end := len(numbers) - 1
|
||||
for i := end; i >= 0; i -= 1 {
|
||||
if numbers[i] < currentNumber {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func AsJson(i interface{}) string {
|
||||
bytes, _ := json.MarshalIndent(i, "", " ")
|
||||
return string(bytes)
|
||||
}
|
||||
530
pkg/utils/utils_test.go
Normal file
530
pkg/utils/utils_test.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSplitLines is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithPadding is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrimTrailingNewline is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeLinefeeds is a function.
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolvePlaceholderString is a function.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDisplayArraysAligned is a function.
|
||||
func TestDisplayArraysAligned(t *testing.T) {
|
||||
type scenario struct {
|
||||
input [][]string
|
||||
expected bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[][]string{{"", ""}, {"", ""}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[][]string{{""}, {"", ""}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
|
||||
}
|
||||
}
|
||||
|
||||
type myDisplayable struct {
|
||||
strings []string
|
||||
}
|
||||
|
||||
type myStruct struct{}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (d *myDisplayable) GetDisplayStrings() []string {
|
||||
return d.strings
|
||||
}
|
||||
|
||||
// TestGetDisplayStringArrays is a function.
|
||||
func TestGetDisplayStringArrays(t *testing.T) {
|
||||
type scenario struct {
|
||||
input []Displayable
|
||||
expected [][]string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[]Displayable{
|
||||
Displayable(&myDisplayable{[]string{"a", "b"}}),
|
||||
Displayable(&myDisplayable{[]string{"c", "d"}}),
|
||||
},
|
||||
[][]string{{"a", "b"}, {"c", "d"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderDisplayableList is a function.
|
||||
func TestRenderDisplayableList(t *testing.T) {
|
||||
type scenario struct {
|
||||
input []Displayable
|
||||
expectedString string
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[]Displayable{
|
||||
Displayable(&myDisplayable{[]string{}}),
|
||||
Displayable(&myDisplayable{[]string{}}),
|
||||
},
|
||||
"\n",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]Displayable{
|
||||
Displayable(&myDisplayable{[]string{"aa", "b"}}),
|
||||
Displayable(&myDisplayable{[]string{"c", "d"}}),
|
||||
},
|
||||
"aa b\nc d",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]Displayable{
|
||||
Displayable(&myDisplayable{[]string{"a"}}),
|
||||
Displayable(&myDisplayable{[]string{"b", "c"}}),
|
||||
},
|
||||
"",
|
||||
errors.New("Each item must return the same number of strings to display"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
str, err := renderDisplayableList(s.input)
|
||||
assert.EqualValues(t, s.expectedString, str)
|
||||
assert.EqualValues(t, s.expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRenderList is a function.
|
||||
func TestRenderList(t *testing.T) {
|
||||
type scenario struct {
|
||||
input interface{}
|
||||
expectedString string
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[]*myDisplayable{
|
||||
{[]string{"aa", "b"}},
|
||||
{[]string{"c", "d"}},
|
||||
},
|
||||
"aa b\nc d",
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]*myStruct{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
"",
|
||||
errors.New("item does not implement the Displayable interface"),
|
||||
},
|
||||
{
|
||||
&myStruct{},
|
||||
"",
|
||||
errors.New("RenderList given a non-slice type"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
str, err := RenderList(s.input)
|
||||
assert.EqualValues(t, s.expectedString, str)
|
||||
assert.EqualValues(t, s.expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPaddedDisplayStrings is a function.
|
||||
func TestGetPaddedDisplayStrings(t *testing.T) {
|
||||
type scenario struct {
|
||||
stringArrays [][]string
|
||||
padWidths []int
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[][]string{{"a", "b"}, {"c", "d"}},
|
||||
[]int{1},
|
||||
[]string{"a b", "c d"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPadWidths is a function.
|
||||
func TestGetPadWidths(t *testing.T) {
|
||||
type scenario struct {
|
||||
stringArrays [][]string
|
||||
expected []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[][]string{{""}, {""}},
|
||||
[]int{},
|
||||
},
|
||||
{
|
||||
[][]string{{"a"}, {""}},
|
||||
[]int{},
|
||||
},
|
||||
{
|
||||
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
|
||||
[]int{2, 1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
|
||||
}
|
||||
}
|
||||
|
||||
// TestMin is a function.
|
||||
func TestMin(t *testing.T) {
|
||||
type scenario struct {
|
||||
a int
|
||||
b int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
},
|
||||
{
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
},
|
||||
{
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, Min(s.a, s.b))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIncludesString is a function.
|
||||
func TestIncludesString(t *testing.T) {
|
||||
type scenario struct {
|
||||
list []string
|
||||
element string
|
||||
expected bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
"a",
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
"c",
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"a", "b"},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{""},
|
||||
"",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextIndex(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
list []int
|
||||
element int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
// I'm not really fussed about how it behaves here
|
||||
"no elements",
|
||||
[]int{},
|
||||
1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"one element",
|
||||
[]int{1},
|
||||
1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"two elements",
|
||||
[]int{1, 2},
|
||||
1,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"two elements, giving second one",
|
||||
[]int{1, 2},
|
||||
2,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"three elements, giving second one",
|
||||
[]int{1, 2, 3},
|
||||
2,
|
||||
2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrevIndex(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
list []int
|
||||
element int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
// I'm not really fussed about how it behaves here
|
||||
"no elements",
|
||||
[]int{},
|
||||
1,
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"one element",
|
||||
[]int{1},
|
||||
1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"two elements",
|
||||
[]int{1, 2},
|
||||
1,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"three elements, giving second one",
|
||||
[]int{1, 2, 3},
|
||||
2,
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsJson(t *testing.T) {
|
||||
type myStruct struct {
|
||||
a string
|
||||
}
|
||||
|
||||
output := AsJson(&myStruct{a: "foo"})
|
||||
|
||||
// no idea why this is returning empty hashes but it's works in the app ¯\_(ツ)_/¯
|
||||
assert.EqualValues(t, "{}", output)
|
||||
}
|
||||
5
scripts/bump_modules.sh
Executable file
5
scripts/bump_modules.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
GO111MODULE=on
|
||||
mv go.mod /tmp/
|
||||
go mod init
|
||||
63
scripts/generate_cheatsheet.go
Normal file
63
scripts/generate_cheatsheet.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// This "script" generates a file called Keybindings_{{.LANG}}.md
|
||||
// in current working directory.
|
||||
//
|
||||
// The content of this generated file is a keybindings cheatsheet.
|
||||
//
|
||||
// To generate cheatsheet in english run:
|
||||
// LANG=en go run scripts/generate_cheatsheet.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func writeString(file *os.File, str string) {
|
||||
_, err := file.WriteString(str)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getTitle(mApp *app.App, viewName string) string {
|
||||
viewTitle := strings.Title(viewName) + "Title"
|
||||
translatedTitle := mApp.Tr.SLocalize(viewTitle)
|
||||
formattedTitle := fmt.Sprintf("\n## %s\n\n", translatedTitle)
|
||||
return formattedTitle
|
||||
}
|
||||
|
||||
func main() {
|
||||
mConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
|
||||
mApp, _ := app.Setup(mConfig)
|
||||
lang := mApp.Tr.GetLanguage()
|
||||
file, _ := os.Create("Keybindings_" + lang + ".md")
|
||||
current := ""
|
||||
|
||||
writeString(file, fmt.Sprintf("# Lazygit %s\n", mApp.Tr.SLocalize("menu")))
|
||||
writeString(file, getTitle(mApp, "global"))
|
||||
|
||||
writeString(file, "<pre>\n")
|
||||
|
||||
for _, binding := range mApp.Gui.GetKeybindings() {
|
||||
if binding.Description == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if binding.ViewName != current {
|
||||
current = binding.ViewName
|
||||
writeString(file, "</pre>\n")
|
||||
writeString(file, getTitle(mApp, current))
|
||||
writeString(file, "<pre>\n")
|
||||
}
|
||||
|
||||
info := fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
|
||||
writeString(file, info)
|
||||
}
|
||||
|
||||
writeString(file, "</pre>\n")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// call from project root with
|
||||
// go run bin/push_new_patch.go
|
||||
// go run scripts/push_new_patch/main.go
|
||||
|
||||
// goreleaser expects a $GITHUB_TOKEN env variable to be defined
|
||||
// in order to push the release got github
|
||||
@@ -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",
|
||||
"g": "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,41 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 {
|
||||
fmt.Fprint(v, coloredString(" (merging)", color.FgYellow))
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
branch := branches[0]
|
||||
name := coloredString(branch.Name, branch.getColor())
|
||||
repo := getCurrentProject()
|
||||
fmt.Fprint(v, " "+repo+" → "+name)
|
||||
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
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# this script makes a repo with lots of commits
|
||||
|
||||
# call this command from the test directory:
|
||||
# ./lots_of_commits.sh; cd testrepo; gg; cd ..
|
||||
|
||||
# -e means exit if something fails
|
||||
# -x means print out simple commands before running them
|
||||
set -ex
|
||||
|
||||
reponame="case_insensitive_checkouts"
|
||||
|
||||
rm -rf ${reponame}
|
||||
mkdir ${reponame}
|
||||
cd ${reponame}
|
||||
|
||||
git init
|
||||
|
||||
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
|
||||
25
test/hooks/pre-push
Normal file
25
test/hooks/pre-push
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# test pre-push hook for testing the lazygit credentials view
|
||||
#
|
||||
# to enable, use:
|
||||
# chmod +x .git/hooks/pre-push
|
||||
#
|
||||
# this will hang if you're using git from the command line, so only enable this
|
||||
# when you are testing the credentials view in lazygit
|
||||
|
||||
exec < /dev/tty
|
||||
|
||||
echo -n "Username for 'github': "
|
||||
read username
|
||||
|
||||
echo -n "Password for 'github': "
|
||||
read password
|
||||
|
||||
if [ "$username" = "username" -a "$password" = "password" ]; then
|
||||
echo "success"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
>&2 echo "incorrect username/password"
|
||||
exit 1
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# this script makes a repo with lots of commits
|
||||
|
||||
# call this command from the test directory:
|
||||
# ./lots_of_commits.sh; cd testrepo; gg; cd ..
|
||||
|
||||
# -e means exit if something fails
|
||||
# -x means print out simple commands before running them
|
||||
set -ex
|
||||
|
||||
reponame="lots_of_commits"
|
||||
|
||||
rm -rf ${reponame}
|
||||
mkdir ${reponame}
|
||||
cd ${reponame}
|
||||
|
||||
git init
|
||||
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user