Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63e2ccfccf | ||
|
|
6fd4d49db7 | ||
|
|
2393bc791d | ||
|
|
398f91decb | ||
|
|
c937a93f79 | ||
|
|
9402d8b0c0 | ||
|
|
2a0615da10 | ||
|
|
4be5eaae7b | ||
|
|
038dcb546e | ||
|
|
1184990e16 | ||
|
|
ac5088eee6 | ||
|
|
e36899d5c5 | ||
|
|
403526bc50 | ||
|
|
a5d27764cd | ||
|
|
43758cbb5f | ||
|
|
0079015102 | ||
|
|
7a2176f479 | ||
|
|
e0bdfad63a | ||
|
|
f07fc31f8b | ||
|
|
4bb577ab7d | ||
|
|
8305d8e72f | ||
|
|
f68166e858 | ||
|
|
8925b161a7 | ||
|
|
0a1298765c | ||
|
|
273678f081 | ||
|
|
790235f64b | ||
|
|
abc0f7f0aa | ||
|
|
ab81f27fc7 | ||
|
|
dbb01b028d | ||
|
|
0c886eddfb | ||
|
|
399346c2ee | ||
|
|
7a170bbccf | ||
|
|
8c0ea8f45f | ||
|
|
afbc028ad6 | ||
|
|
e331dfcaf8 | ||
|
|
1337f6e76a | ||
|
|
4de31da4be | ||
|
|
23c51ba708 | ||
|
|
19a3ac603d | ||
|
|
f4938deaae | ||
|
|
639df512f3 | ||
|
|
a8858cbd12 | ||
|
|
1a19b1412d | ||
|
|
95d451e59a | ||
|
|
6c1d2d45ef | ||
|
|
f6b3a9b184 | ||
|
|
cdc50e8557 | ||
|
|
0173fdb9df | ||
|
|
9661ea04f3 | ||
|
|
0228e25084 | ||
|
|
935f774834 | ||
|
|
dcc7855fd0 | ||
|
|
a8e22ed82f | ||
|
|
d44638130c | ||
|
|
76a27f417f | ||
|
|
adc2529019 | ||
|
|
43ab7318d3 | ||
|
|
cb372d469f | ||
|
|
88ba6efdd5 | ||
|
|
e011e9bc42 | ||
|
|
ad93b4c863 | ||
|
|
198cbee498 | ||
|
|
daca07eaca | ||
|
|
34acaf7ac4 | ||
|
|
d967f65329 | ||
|
|
306ac41fd8 | ||
|
|
c101993405 | ||
|
|
6430ab6ac9 | ||
|
|
e09f3905e9 | ||
|
|
53e73313a2 | ||
|
|
0891797bf8 | ||
|
|
cfe3605e6b | ||
|
|
75ab8ec4d9 | ||
|
|
77faf85cfc | ||
|
|
3d343e9b57 | ||
|
|
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 | ||
|
|
a365615490 | ||
|
|
9489a94473 | ||
|
|
e0ff46fe53 | ||
|
|
cce6f405a5 | ||
|
|
e39d2ed44b | ||
|
|
7a7e885773 | ||
|
|
34fd18a395 | ||
|
|
a1ee11e54e | ||
|
|
7b850c56c4 | ||
|
|
88c01c1ded | ||
|
|
27994f7de8 | ||
|
|
670f0e37c7 | ||
|
|
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 | ||
|
|
7bfd8155c2 | ||
|
|
ed1518fd66 | ||
|
|
400301e8fa | ||
|
|
f97a098c6f | ||
|
|
2b3bdc8b8f | ||
|
|
7323e08fdb | ||
|
|
838ed4f170 | ||
|
|
feba6b5318 | ||
|
|
27234c1cab | ||
|
|
8f6329c402 | ||
|
|
0ac56f5ade | ||
|
|
471a7de418 | ||
|
|
77191ea67c | ||
|
|
176aa62fe0 | ||
|
|
1419be1aab | ||
|
|
0ac306fe2a | ||
|
|
f0840c0f46 | ||
|
|
6d4d6d3ac0 | ||
|
|
2dba6f6733 | ||
|
|
aec125df70 | ||
|
|
9626ebdf35 | ||
|
|
2c140445e5 | ||
|
|
62a231abb7 | ||
|
|
753ca75e55 | ||
|
|
5c8f0d4c9d | ||
|
|
000a709783 | ||
|
|
5c185572a7 | ||
|
|
bb86d527e1 | ||
|
|
0e3dec1069 | ||
|
|
7e141283f6 | ||
|
|
e46f3b3393 | ||
|
|
73e740d1ba | ||
|
|
f1eaeec9ee | ||
|
|
a713fff362 | ||
|
|
6d3d40c41f | ||
|
|
2137d3f6d2 | ||
|
|
8ae346787a | ||
|
|
b8daf71db6 | ||
|
|
fd13cf14b4 | ||
|
|
47bf649a69 | ||
|
|
3b018e040f | ||
|
|
b2fbccd392 | ||
|
|
542fd8e88a | ||
|
|
9fbc332c9a | ||
|
|
1ef794e09f | ||
|
|
bfa47d3b91 | ||
|
|
48cea4e1c4 | ||
|
|
7aa884ed8f | ||
|
|
7eb673e574 | ||
|
|
f20121cb0b | ||
|
|
aa4160d57a | ||
|
|
8bc6832546 | ||
|
|
4853d186ee | ||
|
|
c3611df8d8 | ||
|
|
59650cff26 | ||
|
|
f9abcdb159 | ||
|
|
28505dddaf | ||
|
|
95b7c1d0a4 | ||
|
|
eb9f01ecfa | ||
|
|
a8c6f31bc6 | ||
|
|
1fded005c4 | ||
|
|
c470c1f575 | ||
|
|
d08241b2ea | ||
|
|
de7ab80539 | ||
|
|
d3abd9eab1 | ||
|
|
3f89b5bf71 | ||
|
|
87872f5514 | ||
|
|
bf9a53fada | ||
|
|
3adf5368dd | ||
|
|
ed6f21ee74 | ||
|
|
75a186614a | ||
|
|
18aa5b1bbe | ||
|
|
89209d1253 | ||
|
|
bcad80250a | ||
|
|
44eaccfd14 | ||
|
|
9537645d0c | ||
|
|
d08e3a55a1 | ||
|
|
2cd0bd8125 | ||
|
|
cc31cb1abe | ||
|
|
a56643fe64 | ||
|
|
2386283e45 | ||
|
|
58977ed7f3 | ||
|
|
a068548bcb | ||
|
|
b2f8b8b345 | ||
|
|
5f70b2d9cd | ||
|
|
f33b2b2277 | ||
|
|
dbf65a422a | ||
|
|
ace8544512 | ||
|
|
4832d365f1 | ||
|
|
bebe94b4b3 | ||
|
|
750445dc8b | ||
|
|
1f5f80b1bf | ||
|
|
5cc34e7801 | ||
|
|
ce8884f509 | ||
|
|
481a05f116 | ||
|
|
6e8abbcdda | ||
|
|
273eb6244b | ||
|
|
467741fa54 | ||
|
|
a00bbf709f | ||
|
|
b90e66dca1 | ||
|
|
77eea90b0b | ||
|
|
79b012990b | ||
|
|
45c0b9404a | ||
|
|
50924f31b0 | ||
|
|
705d273ad3 | ||
|
|
30c6fc2300 | ||
|
|
16b202f227 | ||
|
|
7c1dbb7c66 | ||
|
|
41152881db | ||
|
|
d33fff54a3 | ||
|
|
3bf621de65 | ||
|
|
0c84055abd | ||
|
|
5d9df3ca07 | ||
|
|
6581bec1ce | ||
|
|
ce3f81a2d3 | ||
|
|
bc479a9bf5 | ||
|
|
83a9a6d1f5 | ||
|
|
ca0b3debe9 | ||
|
|
84a2b1ceb9 | ||
|
|
5951811a8b | ||
|
|
968f3b2c1c | ||
|
|
aacbb72e6c | ||
|
|
48ab294922 | ||
|
|
555996642d | ||
|
|
aad4269128 | ||
|
|
b3fbe8453f | ||
|
|
ffac0e6420 | ||
|
|
7371765d47 | ||
|
|
7f75808499 | ||
|
|
778cb14dc4 | ||
|
|
00c679a3f0 | ||
|
|
d9ec6f9890 | ||
|
|
9858bfe4c3 | ||
|
|
feada164d0 | ||
|
|
a1706ef2ee | ||
|
|
1bd3aa9708 | ||
|
|
d0aaf94068 | ||
|
|
8054d85782 | ||
|
|
31a8be5f32 | ||
|
|
ca04655a09 | ||
|
|
4281cc2884 | ||
|
|
8013f18177 | ||
|
|
c965333756 | ||
|
|
54408a912c | ||
|
|
3839719154 | ||
|
|
e8c4bf20f6 | ||
|
|
fbadbdd771 | ||
|
|
c6aee678c0 | ||
|
|
c4c27262f2 | ||
|
|
a253b0b897 | ||
|
|
5ca47c3f47 | ||
|
|
456c593c72 | ||
|
|
3b264806ef | ||
|
|
92e75d4602 | ||
|
|
7add598235 | ||
|
|
2853aba951 | ||
|
|
8cc444182f | ||
|
|
2a25ce014b | ||
|
|
411c02324f | ||
|
|
2f50cbf2b8 | ||
|
|
417cb97dc6 | ||
|
|
fb5d25c9e9 | ||
|
|
f580572e70 | ||
|
|
ba30e9e450 | ||
|
|
30d79bfa2c | ||
|
|
4d9d2b134f | ||
|
|
6cb0a14f33 | ||
|
|
51d3e679bf | ||
|
|
bab884f1df | ||
|
|
1730089b09 | ||
|
|
8c823965ab | ||
|
|
aa6a0bd43b | ||
|
|
d781a8e650 | ||
|
|
4528cf95b9 | ||
|
|
dd6cdc02f0 | ||
|
|
2311675d08 | ||
|
|
cda365a93f | ||
|
|
b567a86ee5 | ||
|
|
a6cb0feff1 | ||
|
|
af766c848c | ||
|
|
ade0dc447e | ||
|
|
8aca30ee4c | ||
|
|
d5a73c0166 | ||
|
|
53044e20c9 | ||
|
|
59ea1491db | ||
|
|
69f47fddc4 | ||
|
|
75e1a2d62f | ||
|
|
b715236c0a | ||
|
|
4e8f909141 | ||
|
|
3c82edb301 | ||
|
|
3dafb78b1d | ||
|
|
abfbd59d67 | ||
|
|
fbd377606a | ||
|
|
ee7fad1433 | ||
|
|
6925ceeb6e | ||
|
|
4d425de5d0 | ||
|
|
19af3f967a | ||
|
|
138a5d86ed | ||
|
|
acae855ace | ||
|
|
50e4f74350 | ||
|
|
5a624764a8 | ||
|
|
790d5a1745 | ||
|
|
c81922f8cf | ||
|
|
232e1c74e4 | ||
|
|
2b37c9b19c | ||
|
|
c8bacb4186 | ||
|
|
a1aa031252 | ||
|
|
c4f3637e21 | ||
|
|
6cf6fecb64 | ||
|
|
b541987007 | ||
|
|
2d4801c39d | ||
|
|
7fe54e889d | ||
|
|
f6a9c727fa | ||
|
|
f8ca0ddcad | ||
|
|
138f40d7ae | ||
|
|
b80e8111c6 | ||
|
|
6df4a3ff41 | ||
|
|
a70753364c | ||
|
|
948f6a1f7a | ||
|
|
46fb4c5f0a | ||
|
|
e23ed80eaa | ||
|
|
ab03902d08 | ||
|
|
9bfaa9f62c | ||
|
|
28caaf6f1c | ||
|
|
384d2e3c83 | ||
|
|
a4f4a16237 | ||
|
|
a4a975a69c | ||
|
|
6985e52949 | ||
|
|
eabf07248a | ||
|
|
ba9485ae57 | ||
|
|
236eabb811 | ||
|
|
f11edda6a2 | ||
|
|
7fb77418cc | ||
|
|
dcf87e2765 | ||
|
|
90e488d5a4 | ||
|
|
31f6ef1c29 | ||
|
|
c6ab935779 | ||
|
|
9c2e1dfc6e | ||
|
|
16e3d5f092 | ||
|
|
7dc4878e0b | ||
|
|
694b192b74 | ||
|
|
9e5c04d57d | ||
|
|
91ecf1e499 | ||
|
|
7e5d7bfe38 | ||
|
|
9067c3be3e | ||
|
|
fc2e3a5316 | ||
|
|
17cf4cec31 | ||
|
|
69378b8ff4 | ||
|
|
fbc09e2093 | ||
|
|
afc3d4a997 | ||
|
|
caea649733 | ||
|
|
a02d3a0c4e | ||
|
|
3ee9687540 | ||
|
|
15f982daae | ||
|
|
cf73d4f558 | ||
|
|
b1918f2f68 | ||
|
|
0e0acc90e1 | ||
|
|
f08ffef7f3 | ||
|
|
c38dc16def | ||
|
|
61815013cb | ||
|
|
e5f0a89bf1 | ||
|
|
b1e6735d8d | ||
|
|
6b5c08d449 | ||
|
|
4836e6c906 | ||
|
|
684e8b2e80 | ||
|
|
7b7621c18c | ||
|
|
9a3b565806 | ||
|
|
e8aba2e517 | ||
|
|
da227003c7 | ||
|
|
6126ddd373 | ||
|
|
59885e45df | ||
|
|
6e585c54a2 | ||
|
|
ac8264e9c2 | ||
|
|
1c99aaff43 | ||
|
|
0df11e27fa | ||
|
|
0aa587f55a | ||
|
|
934e7a2e71 | ||
|
|
01540866bf | ||
|
|
8aacd6915b | ||
|
|
67e56e5fbc | ||
|
|
79703c0087 | ||
|
|
74cb78cbe6 | ||
|
|
3c0eeba187 | ||
|
|
e21f840350 | ||
|
|
d780549cea | ||
|
|
12f475544e | ||
|
|
b422c5cc86 | ||
|
|
3790ef3e46 | ||
|
|
5c271b8a70 | ||
|
|
8b0db89edb | ||
|
|
2dfef09e55 | ||
|
|
d6dbd46793 | ||
|
|
001ea1002d | ||
|
|
5dbe262d2d | ||
|
|
f0447be989 | ||
|
|
d832eab20c | ||
|
|
fab194e923 | ||
|
|
a47c889cbb | ||
|
|
61dcbb456d | ||
|
|
37d9ed6712 | ||
|
|
e059a87ae2 | ||
|
|
087cc7362a | ||
|
|
16df4786c0 | ||
|
|
dea751280e | ||
|
|
bcdfe242ac | ||
|
|
0a1c4b7cbe | ||
|
|
42cb8e4abf | ||
|
|
e80be83065 | ||
|
|
b2e0bb49fb | ||
|
|
36df4c825f | ||
|
|
56bf7cfee8 | ||
|
|
76d4c62995 | ||
|
|
4a483c112e | ||
|
|
211715fa77 | ||
|
|
5a143ea500 | ||
|
|
5fd3d11789 | ||
|
|
691972b544 | ||
|
|
21dd06989d | ||
|
|
43f0cf42e6 | ||
|
|
08174ca848 | ||
|
|
5e3481ca58 | ||
|
|
dda94949d4 | ||
|
|
7f01bfa24e | ||
|
|
4750aacb9b | ||
|
|
ea51578f5c | ||
|
|
57611d355c | ||
|
|
595959db52 | ||
|
|
129be0b364 | ||
|
|
30ce316209 | ||
|
|
027ea74ca9 | ||
|
|
37b5bc7cb3 | ||
|
|
336a494862 | ||
|
|
6167eb5cc0 | ||
|
|
d413d4319f | ||
|
|
7653ff9a24 | ||
|
|
4f6ae62e79 | ||
|
|
ceeca40fe5 | ||
|
|
1378143889 | ||
|
|
0d2076c57c | ||
|
|
68a23f14e8 | ||
|
|
0715487730 | ||
|
|
dd511acbdd | ||
|
|
9cd78f719e | ||
|
|
9c018c6138 | ||
|
|
d1ead5b0cf | ||
|
|
164ac72c5a | ||
|
|
d89cab146a | ||
|
|
f2bb6128a9 | ||
|
|
80bcc7c16e | ||
|
|
a0c8fc8899 | ||
|
|
7cdcef8c93 | ||
|
|
5ccea4f5d9 | ||
|
|
157278e06e | ||
|
|
103a6fd219 | ||
|
|
a555a75565 | ||
|
|
b6eaa44cc2 | ||
|
|
ec78c795dd | ||
|
|
787e5d13ed | ||
|
|
843927dd26 | ||
|
|
06713f608b | ||
|
|
112893f73c | ||
|
|
00dca6f76d | ||
|
|
10fd353a50 | ||
|
|
bb12309c7c | ||
|
|
fe99e70983 | ||
|
|
d62f41a4f1 | ||
|
|
301d34ef3d | ||
|
|
796286e98e | ||
|
|
7d70ed5be1 | ||
|
|
f465a5fe48 | ||
|
|
56f9dfd856 | ||
|
|
dfbbad5d95 | ||
|
|
3cb228aac8 | ||
|
|
c7f4af2b41 | ||
|
|
810bd54e68 | ||
|
|
9fc194f0cd | ||
|
|
ac1fa346ac | ||
|
|
30c4c6e576 | ||
|
|
6c81766972 | ||
|
|
cd5a2a3c6d | ||
|
|
b24632caf6 | ||
|
|
4b511863f9 | ||
|
|
023a39a903 | ||
|
|
bcd5e65878 | ||
|
|
4f2b1608b4 |
@@ -0,0 +1,73 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
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" }}-v4
|
||||
- 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" }}-v4
|
||||
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: /.*/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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/
|
||||
|
||||
test/git_server/data
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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
|
||||
- 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: 32-bit
|
||||
amd64: x86_64
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: '{{ .Tag }}-next'
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^bump'
|
||||
brew:
|
||||
# Reporitory to push the tap to.
|
||||
github:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: 'https://github.com/jesseduffield/lazygit/'
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
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
|
||||
@@ -0,0 +1,74 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the [project leader](https://github.com/jesseduffield).
|
||||
All complaints will be reviewed and investigated and will result in a response that
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
@@ -0,0 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
|
||||
♥ We love pull requests from everyone !
|
||||
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish
|
||||
to make via issue, email, or any other method with the owners of this repository
|
||||
before making a change.
|
||||
|
||||
## So all code changes happen through Pull Requests
|
||||
Pull requests are the best way to propose changes to the codebase. We actively
|
||||
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. 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].
|
||||
|
||||
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
In short, when you submit code changes, your submissions are understood to be
|
||||
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
||||
covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
||||
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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 /go/src/github.com/jesseduffield/lazygit
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
RUN echo "alias gg=lazygit" >> ~/.profile
|
||||
@@ -0,0 +1,652 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e24ea5dbc89fbab51635ee32e5be4f61a9267cae20788efcae4c07efb4abec99"
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
"aws",
|
||||
"aws/awserr",
|
||||
"aws/awsutil",
|
||||
"aws/client",
|
||||
"aws/client/metadata",
|
||||
"aws/corehandlers",
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
"private/protocol/restxml",
|
||||
"private/protocol/xml/xmlutil",
|
||||
"service/s3",
|
||||
"service/sts",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "4324bc9d8865bdb3e6aa86ec7772ca1272d2750e"
|
||||
version = "v1.15.21"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:37011b20a70e205b93ebea5287e1afa5618db54bf3998c36ff5a8e4b146a170a"
|
||||
name = "github.com/bgentry/go-netrc"
|
||||
packages = ["netrc"]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
|
||||
name = "github.com/cloudfoundry/jibber_jabber"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "NUT"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
|
||||
name = "github.com/emirpasic/gods"
|
||||
packages = [
|
||||
"containers",
|
||||
"lists",
|
||||
"lists/arraylist",
|
||||
"trees",
|
||||
"trees/binaryheap",
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
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:ea1d5bfdb4ec5c2ee48c97865e6de1a28fa8c4849a3f56b27d521aa619038e06"
|
||||
name = "github.com/go-errors/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "a6af135bd4e28680facf08a3d206b454abc877a4"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[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"
|
||||
name = "github.com/golang-collections/collections"
|
||||
packages = ["stack"]
|
||||
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"
|
||||
name = "github.com/jbenet/go-context"
|
||||
packages = ["io"]
|
||||
pruneopts = "NUT"
|
||||
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:490643e333b848f3d6ab772c21082d706663dcf4a3c0fbe9a4b4ef7b205ce6c7"
|
||||
name = "github.com/jesseduffield/go-getter"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "906e15686e6309ff310c1c10463ab53287c3a678"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:831819e8726b6b19e90079bce76d3048e2be3595359ee912d63d560e0db89b97"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "f0f0ab442e6c8e42650a7e38a45122bba2e11f3f"
|
||||
|
||||
[[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"
|
||||
name = "github.com/kevinburke/ssh_config"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
|
||||
version = "0.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c2353362d570a7bfa228149c62842019201cfb71"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cb591533458f6eb6e2c1065ff3eac6b50263d7847deb23fc9f79b25bc608970e"
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
|
||||
name = "github.com/mgutz/str"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
|
||||
name = "github.com/mitchellh/go-testing-interface"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"v2/i18n",
|
||||
"v2/internal",
|
||||
"v2/internal/plural",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
|
||||
version = "v2.0.0-beta.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
|
||||
name = "github.com/pelletier/go-buffruneio"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "NUT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
pruneopts = "NUT"
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
|
||||
name = "github.com/shibukawa/configdir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||
version = "v1.0.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0e9a5ac14bcc11f205031a671b28c7e05cb88b2ebbe06f383c1ab0b2c12c7cb5"
|
||||
name = "github.com/spkg/bom"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "59b7046e48ad6bac800c5e1dd5142282cbfcf154"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
|
||||
name = "github.com/src-d/gcfg"
|
||||
packages = [
|
||||
".",
|
||||
"scanner",
|
||||
"token",
|
||||
"types",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bacb8b590716ab7c33f2277240972c9582d389593ee8d66fc10074e0508b8126"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
pruneopts = "NUT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e42372d3f4921ec35df07f9b23239631e9d28580f7c1edcca212bc6daddc68fe"
|
||||
name = "github.com/stvp/roll"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3627a5cbeaeaa68023abd02bb8687925265f2f63"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
|
||||
name = "github.com/tcnksm/go-gitconfig"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
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"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cast5",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"openpgp",
|
||||
"openpgp/armor",
|
||||
"openpgp/elgamal",
|
||||
"openpgp/errors",
|
||||
"openpgp/packet",
|
||||
"openpgp/s2k",
|
||||
"poly1305",
|
||||
"ssh",
|
||||
"ssh/agent",
|
||||
"ssh/knownhosts",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context"]
|
||||
pruneopts = "NUT"
|
||||
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:47a697b155f5214ff14e68e39ce9c2e8d93e1fb035ae5ba7e247d044e0ce64e3"
|
||||
name = "gopkg.in/src-d/go-billy.v4"
|
||||
packages = [
|
||||
".",
|
||||
"helper/chroot",
|
||||
"helper/polyfill",
|
||||
"osfs",
|
||||
"util",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "83cf655d40b15b427014d7875d10850f96edba14"
|
||||
version = "v4.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e66078da2bd6e53c72518d7f6ae0c3c8c7f34c0df12c39435ce34a6bce165525"
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"internal/revision",
|
||||
"plumbing",
|
||||
"plumbing/cache",
|
||||
"plumbing/filemode",
|
||||
"plumbing/format/config",
|
||||
"plumbing/format/diff",
|
||||
"plumbing/format/gitignore",
|
||||
"plumbing/format/idxfile",
|
||||
"plumbing/format/index",
|
||||
"plumbing/format/objfile",
|
||||
"plumbing/format/packfile",
|
||||
"plumbing/format/pktline",
|
||||
"plumbing/object",
|
||||
"plumbing/protocol/packp",
|
||||
"plumbing/protocol/packp/capability",
|
||||
"plumbing/protocol/packp/sideband",
|
||||
"plumbing/revlist",
|
||||
"plumbing/storer",
|
||||
"plumbing/transport",
|
||||
"plumbing/transport/client",
|
||||
"plumbing/transport/file",
|
||||
"plumbing/transport/git",
|
||||
"plumbing/transport/http",
|
||||
"plumbing/transport/internal/common",
|
||||
"plumbing/transport/server",
|
||||
"plumbing/transport/ssh",
|
||||
"storage",
|
||||
"storage/filesystem",
|
||||
"storage/filesystem/dotgit",
|
||||
"storage/memory",
|
||||
"utils/binary",
|
||||
"utils/diff",
|
||||
"utils/ioutil",
|
||||
"utils/merkletrie",
|
||||
"utils/merkletrie/filesystem",
|
||||
"utils/merkletrie/index",
|
||||
"utils/merkletrie/internal/frame",
|
||||
"utils/merkletrie/noder",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf"
|
||||
name = "gopkg.in/warnings.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/cloudfoundry/jibber_jabber",
|
||||
"github.com/fatih/color",
|
||||
"github.com/go-errors/errors",
|
||||
"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/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
@@ -0,0 +1,50 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
non-go = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/golang-collections/collections"
|
||||
|
||||
[[constraint]]
|
||||
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"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Jesse Duffield
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,156 @@
|
||||
# lazygit [](https://circleci.com/gh/jesseduffield/lazygit) [](https://codecov.io/gh/jesseduffield/lazygit) [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/github.com/jesseduffield/lazygit) []()
|
||||
|
||||
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
|
||||
|
||||
Are YOU tired of typing every git command directly into the terminal, but you're
|
||||
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
|
||||
Jira? This is the app for you!
|
||||
|
||||

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

|
||||
|
||||
### Interactive Rebasing
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
is my first project in Go the code could no doubt use an increase in quality,
|
||||
but I'll be improving on it whenever I find the time. If you have any feedback
|
||||
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
|
||||
|
||||
## Social
|
||||
|
||||
If you want to see what I (Jesse) am up to in terms of development, follow me on
|
||||
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
|
||||
[twitch](https://www.twitch.tv/jesseduffield).
|
||||
|
||||
## Alternatives
|
||||
|
||||
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
|
||||
|
||||
- [tig](https://github.com/jonas/tig)
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 820 KiB |
|
Before Width: | Height: | Size: 6.9 MiB |
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 11 MiB |
|
Before Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 431 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 321 KiB |
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 962 KiB |
|
Before Width: | Height: | Size: 7.0 MiB |
|
Before Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 611 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 343 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,83 @@
|
||||
# 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
|
||||
git:
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
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:
|
||||
|
||||

|
||||
@@ -0,0 +1,114 @@
|
||||
# Lazygit menu
|
||||
|
||||
## Global
|
||||
|
||||
<pre>
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: refresh
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>s</kbd>: switch to a recent repo
|
||||
</pre>
|
||||
|
||||
## Files
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: delete if untracked / checkout if tracked
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>S</kbd>: stash files
|
||||
<kbd>s</kbd>: soft reset to last commit
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>t</kbd>: add patch
|
||||
<kbd>D</kbd>: reset hard and remove untracked files
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>r</kbd>: reword commit
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>J</kbd>: move commit down one
|
||||
<kbd>K</kbd>: move commit up one
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
</pre>
|
||||
|
||||
## Main (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll down
|
||||
<kbd>PgUp</kbd>: scroll up
|
||||
</pre>
|
||||
|
||||
## Main (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: stage line
|
||||
<kbd>a</kbd>: stage hunk
|
||||
</pre>
|
||||
|
||||
## Main (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,66 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.12
|
||||
|
||||
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-errors/errors v1.0.1
|
||||
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-20190305102456-f0f0ab442e6c
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
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-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
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-20190305102456-f0f0ab442e6c h1:ZmCZFQukfddcISnY8R5YYMCYJX0mc1Wfmq/MhEQViUE=
|
||||
github.com/jesseduffield/gocui v0.0.0-20190305102456-f0f0ab442e6c/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=
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
var (
|
||||
commit string
|
||||
version = "unversioned"
|
||||
date string
|
||||
buildSource = "unknown"
|
||||
|
||||
configFlag = flag.Bool("config", false, "Print the current default config")
|
||||
debuggingFlag = flag.Bool("debug", false, "a boolean")
|
||||
versionFlag = flag.Bool("v", false, "Print the current version")
|
||||
)
|
||||
|
||||
func projectPath(path string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
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)
|
||||
}
|
||||
|
||||
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.NewApp(appConfig)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
ClientContext string
|
||||
}
|
||||
|
||||
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() || os.Getenv("DEBUG") == "TRUE" {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.Tr = i18n.NewLocalizer(app.Log)
|
||||
|
||||
// if we are being called in 'demon' mode, we can just return here
|
||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||
if app.ClientContext != "" {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
||||
|
||||
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, app.Config)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Not a git repository") {
|
||||
fmt.Println("Not in a git repository. Use `git init` to create a new one")
|
||||
os.Exit(1)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
if app.ClientContext == "INTERACTIVE_REBASE" {
|
||||
return app.Rebase()
|
||||
}
|
||||
|
||||
if app.ClientContext == "EXIT_IMMEDIATELY" {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return app.Gui.RunWithSubprocesses()
|
||||
}
|
||||
|
||||
// Rebase contains logic for when we've been run in demon mode, meaning we've
|
||||
// given lazygit as a command for git to call e.g. to edit a file
|
||||
func (app *App) Rebase() error {
|
||||
app.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
app.Log.Info("args: ", os.Args)
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
|
||||
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
|
||||
// but in this case we don't need to edit it, so we'll just return
|
||||
} else {
|
||||
app.Log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
@@ -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(isFocused bool) []string {
|
||||
displayName := utils.ColoredString(b.Name, b.GetColor())
|
||||
if isFocused && 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]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", or "rebasing"
|
||||
DisplayString string
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Copied bool // to know if this commit is ready to be cherry-picked somewhere
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
blue := color.New(color.FgBlue)
|
||||
cyan := color.New(color.FgCyan)
|
||||
white := color.New(color.FgWhite)
|
||||
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
copied := color.New(color.FgCyan, color.BgBlue)
|
||||
|
||||
var shaColor *color.Color
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = red
|
||||
case "pushed":
|
||||
shaColor = yellow
|
||||
case "merged":
|
||||
shaColor = green
|
||||
case "rebasing":
|
||||
shaColor = blue
|
||||
default:
|
||||
shaColor = white
|
||||
}
|
||||
|
||||
if c.Copied {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
actionString := ""
|
||||
if c.Action != "" {
|
||||
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.Sha), actionString + white.Sprint(c.Name)}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/viper"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// This file exports dummy constructors for use by tests in other packages
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
|
||||
}
|
||||
|
||||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
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
|
||||
}
|
||||
|
||||
// NewDummyLog creates a new dummy Log for testing
|
||||
func NewDummyLog() *logrus.Entry {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
return log.WithField("test", "test")
|
||||
}
|
||||
|
||||
// NewDummyGitCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommand() *GitCommand {
|
||||
return NewDummyGitCommandWithOSCommand(NewDummyOSCommand())
|
||||
}
|
||||
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *OSCommand) *GitCommand {
|
||||
return &GitCommand{
|
||||
Log: NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewLocalizer(NewDummyLog()),
|
||||
Config: NewDummyAppConfig(),
|
||||
getGlobalGitConfig: func(string) (string, error) { return "", nil },
|
||||
getLocalGitConfig: func(string) (string, error) { return "", nil },
|
||||
removeFile: func(string) error { return nil },
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"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 := c.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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
HasInlineMergeConflicts bool
|
||||
DisplayString string
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
func (f *File) GetDisplayStrings(isFocused bool) []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}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"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 errors.Wrap(err, 0)
|
||||
}
|
||||
|
||||
if err = chdir(".."); err != nil {
|
||||
return errors.Wrap(err, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Config config.AppConfigurer
|
||||
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, config config.AppConfigurer) (*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,
|
||||
Config: config,
|
||||
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" || change == "AA" || change == "DU",
|
||||
HasInlineMergeConflicts: change == "UU" || change == "AA",
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)))
|
||||
}
|
||||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (c *GitCommand) RebaseBranch(branchName string) error {
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
// 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) (*exec.Cmd, error) {
|
||||
command := fmt.Sprintf("git commit -m %s", 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)
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit"
|
||||
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)
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
for _, name := range fileNames {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
// and "interactive" for interactive rebase
|
||||
func (c *GitCommand) RebaseMode() (string, error) {
|
||||
exists, err := c.OSCommand.FileExists(".git/rebase-apply")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
return "normal", nil
|
||||
}
|
||||
exists, err = c.OSCommand.FileExists(".git/rebase-merge")
|
||||
if exists {
|
||||
return "interactive", err
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveFile directly
|
||||
func (c *GitCommand) RemoveFile(file *File) error {
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); 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", quotedFileName))
|
||||
}
|
||||
|
||||
// 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", c.OSCommand.Quote(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))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
|
||||
revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
|
||||
if err != nil {
|
||||
// turns out we get an error here when it's the first commit. We'll just return the original show
|
||||
return show, nil
|
||||
}
|
||||
if len(revList) == 0 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
// we want to pull out 1a6a69a and 3b51d7c from this:
|
||||
// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
|
||||
// Merge: 1a6a69a 3b51d7c
|
||||
lines := utils.SplitLines(show)
|
||||
if len(lines) < 2 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
secondLineWords := strings.Split(lines[1], " ")
|
||||
if len(secondLineWords) < 3 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return show + mergeDiff, nil
|
||||
}
|
||||
|
||||
// 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"
|
||||
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
|
||||
fileName := c.OSCommand.Quote(split[len(split)-1])
|
||||
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", c.OSCommand.Quote(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))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
cmd := c.OSCommand.ExecutableFromString(command)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"EDITOR="+c.OSCommand.GetLazygitPath(),
|
||||
)
|
||||
return c.OSCommand.RunExecutable(cmd)
|
||||
}
|
||||
|
||||
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
|
||||
// By default we skip the editor in the case where a commit will be made
|
||||
func (c *GitCommand) GenericMerge(commandType string, command string) error {
|
||||
return c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git %s --%s",
|
||||
commandType,
|
||||
command,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
|
||||
todo, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, false)
|
||||
}
|
||||
|
||||
func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
|
||||
// we must ensure that we have at least two commits after the selected one
|
||||
if len(commits) <= index+2 {
|
||||
// assuming they aren't picking the bottom commit
|
||||
return errors.New(c.Tr.SLocalize("NoRoom"))
|
||||
}
|
||||
|
||||
todo := ""
|
||||
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
|
||||
for _, commit := range orderedCommits {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
|
||||
todo, err := c.GenerateGenericRebaseTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit a todo string to write to the todo file
|
||||
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
|
||||
ex := c.OSCommand.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
if c.OSCommand.Config.GetDebug() == true {
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
|
||||
|
||||
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
|
||||
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
|
||||
"LAZYGIT_REBASE_TODO="+todo,
|
||||
"DEBUG="+debug,
|
||||
"LANG=en_US.UTF-8", // Force using EN as language
|
||||
"LC_ALL=en_US.UTF-8", // Force using EN as language
|
||||
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
cmd.Env = append(cmd.Env, "EDITOR="+ex)
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) HardReset(baseSha string) error {
|
||||
return c.OSCommand.RunCommand("git reset --hard " + baseSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SoftReset(baseSha string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + baseSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, action string) (string, error) {
|
||||
if len(commits) <= index+1 {
|
||||
// assuming they aren't picking the bottom commit
|
||||
return "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
|
||||
}
|
||||
|
||||
todo := ""
|
||||
for i, commit := range commits[0 : index+1] {
|
||||
a := "pick"
|
||||
if i == index {
|
||||
a = action
|
||||
}
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
return todo, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
func (c *GitCommand) AmendTo(sha string) error {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git commit --fixup=%s", sha)); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --autostash --autosquash %s^", sha,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
|
||||
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
|
||||
fileName := ".git/rebase-merge/git-rebase-todo"
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := c.getTodoCommitCount(content)
|
||||
|
||||
// we have the most recent commit at the bottom whereas the todo file has
|
||||
// it at the bottom, so we need to subtract our index from the commit count
|
||||
contentIndex := commitCount - 1 - index
|
||||
splitLine := strings.Split(content[contentIndex], " ")
|
||||
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
|
||||
result := strings.Join(content, "\n")
|
||||
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0644)
|
||||
}
|
||||
|
||||
func (c *GitCommand) getTodoCommitCount(content []string) int {
|
||||
// count lines that are not blank and are not comments
|
||||
commitCount := 0
|
||||
for _, line := range content {
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
commitCount++
|
||||
}
|
||||
}
|
||||
return commitCount
|
||||
}
|
||||
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (c *GitCommand) MoveTodoDown(index int) error {
|
||||
fileName := ".git/rebase-merge/git-rebase-todo"
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := c.getTodoCommitCount(content)
|
||||
contentIndex := commitCount - 1 - index
|
||||
|
||||
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
|
||||
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
|
||||
result := strings.Join(rearrangedContent, "\n")
|
||||
|
||||
return ioutil.WriteFile(fileName, []byte(result), 0644)
|
||||
}
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (c *GitCommand) Revert(sha string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
|
||||
todo := ""
|
||||
for _, commit := range commits {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"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,
|
||||
}
|
||||
}
|
||||
|
||||
// SetCommand sets the command function used by the struct.
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||
c.command = cmd
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
// RunExecutable runs an executable file and returns an error if there was one
|
||||
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
|
||||
_, err := c.RunExecutableWithOutput(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecutableFromString takes a string like `git status` and returns an executable command for it
|
||||
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||
splitCmd := str.ToArgv(commandStr)
|
||||
c.Log.Info(splitCmd)
|
||||
return c.command(splitCmd[0], splitCmd[1:]...)
|
||||
}
|
||||
|
||||
// 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 "", errors.Wrap(err, 0)
|
||||
}
|
||||
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 errors.Wrap(err, 0)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 "", errors.Wrap(err, 0)
|
||||
}
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", errors.Wrap(err, 0)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", errors.Wrap(err, 0)
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
}
|
||||
|
||||
// RemoveFile removes a file at the specified path
|
||||
func (c *OSCommand) RemoveFile(filename string) error {
|
||||
err := os.Remove(filename)
|
||||
return errors.Wrap(err, 0)
|
||||
}
|
||||
|
||||
// FileExists checks whether a file exists at the specified path
|
||||
func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
|
||||
// this is useful if you need to give your command some environment variables
|
||||
// before running it
|
||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||
out, err := cmd.CombinedOutput()
|
||||
outString := string(out)
|
||||
c.Log.Info(outString)
|
||||
if err != nil {
|
||||
if len(outString) == 0 {
|
||||
return err
|
||||
}
|
||||
return errors.New(outString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLazygitPath returns the path of the currently executed file
|
||||
func (c *OSCommand) GetLazygitPath() string {
|
||||
ex, err := os.Executable() // get the executable path for git to use
|
||||
if err != nil {
|
||||
ex = os.Args[0] // fallback to the first call argument if needed
|
||||
}
|
||||
return filepath.ToSlash(ex)
|
||||
}
|
||||
@@ -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: "\"",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package commands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: "windows",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: `\"`,
|
||||
fallbackEscapedQuote: "\\'",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(isFocused bool) []string {
|
||||
return []string{s.DisplayString}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"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
|
||||
}
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debuggingFlag = true
|
||||
}
|
||||
|
||||
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
|
||||
mouseEvents: false # will default to true when the feature is complete
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
git:
|
||||
merging:
|
||||
manualCommit: false
|
||||
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
|
||||
// }
|
||||
@@ -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}}'`)
|
||||
}
|
||||
@@ -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"'`)
|
||||
}
|
||||
@@ -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}}"'`)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
)
|
||||
|
||||
// context:
|
||||
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
||||
// which `git branch -a` gives us, but we also want the recency data that
|
||||
// git reflog gives us.
|
||||
// So we get the HEAD, then append get the reflog branches that intersect with
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
|
||||
// if we find out we need to use one of these functions in the git.go file, we
|
||||
// can just pull them out of here and put them there and then call them from in here
|
||||
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
|
||||
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 := utils.SplitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return uniqueByName(branches)
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
|
||||
bIter, err := b.GitCommand.Repo.Branches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
name := b.Name().Short()
|
||||
branches = append(branches, &commands.Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
|
||||
for _, newBranch := range newBranches {
|
||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||
finalBranches = append(finalBranches, newBranch)
|
||||
}
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
|
||||
for _, safeBranch := range safeBranches {
|
||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||
return safeBranch.Name
|
||||
}
|
||||
}
|
||||
return reflogBranch.Name
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
head := b.obtainCurrentBranch()
|
||||
safeBranches := b.obtainSafeBranches()
|
||||
|
||||
reflogBranches := b.obtainReflogBranches()
|
||||
for i, reflogBranch := range reflogBranches {
|
||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||
}
|
||||
|
||||
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
|
||||
if len(branches) == 0 || branches[0].Name != head.Name {
|
||||
branches = append([]*commands.Branch{head}, branches...)
|
||||
}
|
||||
|
||||
branches[0].Recency = " *"
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []*commands.Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
|
||||
finalBranches := make([]*commands.Branch, 0)
|
||||
for _, branch := range branches {
|
||||
if branchIncluded(branch.Name, finalBranches) {
|
||||
continue
|
||||
}
|
||||
finalBranches = append(finalBranches, branch)
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
// A line will have the form '10 days ago master' so we need to strip out the
|
||||
// useful information from that into timeNumber, timeUnit, and branchName
|
||||
func branchInfoFromLine(line string) (string, string, string) {
|
||||
r := regexp.MustCompile("\\|.*\\s")
|
||||
line = r.ReplaceAllString(line, " ")
|
||||
words := strings.Split(line, " ")
|
||||
return words[0], words[1], words[len(words)-1]
|
||||
}
|
||||
|
||||
func abbreviatedTimeUnit(timeUnit string) string {
|
||||
r := regexp.MustCompile("s$")
|
||||
timeUnit = r.ReplaceAllString(timeUnit, "")
|
||||
timeUnitMap := map[string]string{
|
||||
"hour": "h",
|
||||
"minute": "m",
|
||||
"second": "s",
|
||||
"week": "w",
|
||||
"year": "y",
|
||||
"day": "d",
|
||||
"month": "m",
|
||||
}
|
||||
return timeUnitMap[timeUnit]
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// context:
|
||||
// here we get the commits from git log but format them to show whether they're
|
||||
// unpushed/pushed/merged into the base branch or not, or if they're yet to
|
||||
// be processed as part of a rebase (these won't appear in git log but we
|
||||
// grab them from the rebase-related files in the .git directory to show them
|
||||
|
||||
// if we find out we need to use one of these functions in the git.go file, we
|
||||
// can just pull them out of here and put them there and then call them from in here
|
||||
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *commands.OSCommand
|
||||
Tr *i18n.Localizer
|
||||
CherryPickedCommits []*commands.Commit
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit) (*CommitListBuilder, error) {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
CherryPickedCommits: cherryPickedCommits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
commits := []*commands.Commit{}
|
||||
var rebasingCommits []*commands.Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rebaseMode != "" {
|
||||
// here we want to also prepend the commits that we're in the process of rebasing
|
||||
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
}
|
||||
|
||||
unpushedCommits := c.getUnpushedCommits()
|
||||
log := c.getLog()
|
||||
|
||||
// now we can split it up and turn it into commits
|
||||
for _, line := range utils.SplitLines(log) {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
_, unpushed := unpushedCommits[sha]
|
||||
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, &commands.Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Status: status,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
})
|
||||
}
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = c.setCommitMergedStatuses(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commits, err = c.setCommitCherryPickStatuses(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case "normal":
|
||||
return c.getNormalRebasingCommits()
|
||||
case "interactive":
|
||||
return c.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
|
||||
if err == nil {
|
||||
content := string(bytesContent)
|
||||
rewrittenCount = len(strings.Split(content, "\n"))
|
||||
}
|
||||
|
||||
// we know we're rebasing, so lets get all the files whose names have numbers
|
||||
commits := []*commands.Commit{}
|
||||
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
|
||||
if rewrittenCount > 0 {
|
||||
rewrittenCount--
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re := regexp.MustCompile(`^\d+$`)
|
||||
if !re.MatchString(f.Name()) {
|
||||
return nil
|
||||
}
|
||||
bytesContent, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := string(bytesContent)
|
||||
commit, err := c.commitFromPatch(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commits = append([]*commands.Commit{commit}, commits...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// git-rebase-todo example:
|
||||
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
|
||||
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
|
||||
|
||||
// git-rebase-todo.backup example:
|
||||
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
|
||||
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
|
||||
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
|
||||
|
||||
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
|
||||
// and extracts out the sha and names of commits that we still have to go
|
||||
// in the rebase:
|
||||
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
|
||||
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
|
||||
if err != nil {
|
||||
c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error()))
|
||||
// we assume an error means the file doesn't exist so we just return
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commits := []*commands.Commit{}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*commands.Commit{{
|
||||
Sha: splitLine[1][0:7],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
Status: "rebasing",
|
||||
Action: splitLine[0],
|
||||
}}, commits...)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// assuming the file starts like this:
|
||||
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
|
||||
// From: Lazygit Tester <test@example.com>
|
||||
// Date: Wed, 5 Dec 2018 21:03:23 +1100
|
||||
// Subject: second commit on master
|
||||
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
sha := strings.Split(lines[0], " ")[1][0:7]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
return &commands.Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.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
|
||||
}
|
||||
if commit.Status != "pushed" {
|
||||
continue
|
||||
}
|
||||
if passedAncestor {
|
||||
commits[i].Status = "merged"
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
|
||||
for _, commit := range commits {
|
||||
for _, cherryPickedCommit := range c.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
||||
commit.Copied = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getMergeBase() (string, error) {
|
||||
currentBranch, err := c.GitCommand.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
|
||||
}
|
||||
|
||||
// getUnpushedCommits 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 *CommitListBuilder) getUnpushedCommits() 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
|
||||
}
|
||||
|
||||
// getLog gets the git log (currently limited to 30 commits for performance
|
||||
// until we work out lazy loading
|
||||
func (c *CommitListBuilder) 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
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
|
||||
func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
osCommand := commands.NewDummyOSCommand()
|
||||
|
||||
return &CommitListBuilder{
|
||||
Log: commands.NewDummyLog(),
|
||||
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
|
||||
CherryPickedCommits: []*commands.Commit{},
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetUnpushedCommits is a function.
|
||||
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(map[string]bool)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Can't retrieve pushable commits",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(pushables map[string]bool) {
|
||||
assert.EqualValues(t, map[string]bool{}, pushables)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Retrieve pushable commits",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "8a2bb0e\n78976bc")
|
||||
},
|
||||
func(pushables map[string]bool) {
|
||||
assert.Len(t, pushables, 2)
|
||||
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getUnpushedCommits())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetMergeBase is a function.
|
||||
func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"swallows an error if the call to merge-base returns an error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("test")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns the commit when master",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks against develop when a feature branch",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "feature/test")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
|
||||
return exec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getMergeBase())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetLog is a function.
|
||||
func TestCommitListBuilderGetLog(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Retrieves logs",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
|
||||
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when retrieving logs",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string) {
|
||||
assert.Empty(t, output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getLog())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetCommits is a function.
|
||||
func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*commands.Commit, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No data found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("test")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"GetCommits returns 2 commits, 1 unpushed, the other merged",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo", "8a2bb0e")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "78976bc")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
assert.EqualValues(t, []*commands.Commit{
|
||||
{
|
||||
Sha: "8a2bb0e",
|
||||
Name: "commit 1",
|
||||
Status: "unpushed",
|
||||
DisplayString: "8a2bb0e commit 1",
|
||||
},
|
||||
{
|
||||
Sha: "78976bc",
|
||||
Name: "commit 2",
|
||||
Status: "merged",
|
||||
DisplayString: "78976bc commit 2",
|
||||
},
|
||||
}, commits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"GetCommits bubbles up an error from setCommitMergedStatuses",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo", "8a2bb0e")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "78976bc")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
// here's where we are returning the error
|
||||
return exec.Command("test")
|
||||
case "rev-parse":
|
||||
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
|
||||
// here too
|
||||
return exec.Command("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.GetCommits())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"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
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyPatchModifier constructs a new dummy patch modifier for testing
|
||||
func NewDummyPatchModifier() *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: commands.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyPatchParser constructs a new dummy patch parser for testing
|
||||
func NewDummyPatchParser() *PatchParser {
|
||||
return &PatchParser{
|
||||
Log: commands.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||