Compare commits
1522 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5504fa5d0 | ||
|
|
273aba38d4 | ||
|
|
cab0aa462c | ||
|
|
b03e2270a0 | ||
|
|
21049be233 | ||
|
|
f89c47b83d | ||
|
|
44f1f22068 | ||
|
|
a229547048 | ||
|
|
4f700c23ba | ||
|
|
b69fc19b35 | ||
|
|
cd1d1996df | ||
|
|
963fcc1444 | ||
|
|
c6825e3d0d | ||
|
|
20bdba15f6 | ||
|
|
e636857057 | ||
|
|
1ae8523098 | ||
|
|
8eb802d3a0 | ||
|
|
6fc031c523 | ||
|
|
8c93289a72 | ||
|
|
b1df0fafa2 | ||
|
|
15046a0454 | ||
|
|
fb9b6314a0 | ||
|
|
0719a3e36e | ||
|
|
a3b0efb82e | ||
|
|
bde324820d | ||
|
|
bbdbbd0b1b | ||
|
|
d4f3b292e6 | ||
|
|
39eb937830 | ||
|
|
fbab5bd444 | ||
|
|
12ca922a41 | ||
|
|
f4e552f982 | ||
|
|
94d26d00ba | ||
|
|
d80d1f8493 | ||
|
|
ace4350319 | ||
|
|
4441cf1045 | ||
|
|
cf99b47ec0 | ||
|
|
546eb50bac | ||
|
|
5e094c8a7c | ||
|
|
c683f2c96c | ||
|
|
e5a372fa2d | ||
|
|
02f45b679f | ||
|
|
b1cda65dcf | ||
|
|
74ce65d9ff | ||
|
|
ccebe5e069 | ||
|
|
b6ec667de0 | ||
|
|
390b7ddc5e | ||
|
|
38739b16bc | ||
|
|
27525f1d42 | ||
|
|
43a9dc48e0 | ||
|
|
440eb387d7 | ||
|
|
28ffaf9348 | ||
|
|
d7da6dde0e | ||
|
|
e000620cdf | ||
|
|
f09309485a | ||
|
|
e04e2ebab5 | ||
|
|
91a107eb6f | ||
|
|
5ce9e0193a | ||
|
|
4c71c26593 | ||
|
|
abdd2455bb | ||
|
|
c33f8d2790 | ||
|
|
8e9d08bc10 | ||
|
|
9593129e6a | ||
|
|
267da3b4db | ||
|
|
c9ded489c9 | ||
|
|
4c73d070ac | ||
|
|
121b9d0715 | ||
|
|
fbb33b7abc | ||
|
|
7178bab6b4 | ||
|
|
2d7452bfaa | ||
|
|
b0f3bfef27 | ||
|
|
7bc6dc5cf3 | ||
|
|
ee7b634dce | ||
|
|
b0bd752180 | ||
|
|
4d14af5d4b | ||
|
|
7953e58c74 | ||
|
|
549d73a0b1 | ||
|
|
8301bba8ad | ||
|
|
78f17aa541 | ||
|
|
7578a7466f | ||
|
|
8681a6b4e2 | ||
|
|
efed313721 | ||
|
|
795cf39ddf | ||
|
|
f08f248cb7 | ||
|
|
3c20425649 | ||
|
|
dfc689411b | ||
|
|
2295407a45 | ||
|
|
828a2acd26 | ||
|
|
843b8ceab0 | ||
|
|
011451464f | ||
|
|
32d170621c | ||
|
|
464d022a86 | ||
|
|
6a0066253f | ||
|
|
d627b3bfc8 | ||
|
|
952c62df37 | ||
|
|
b6cc1c9492 | ||
|
|
39ae122304 | ||
|
|
c34c6926d5 | ||
|
|
4fe512ff3a | ||
|
|
4197921465 | ||
|
|
4b69ab08c1 | ||
|
|
f3a0058eb9 | ||
|
|
633b6f596d | ||
|
|
e6274c0757 | ||
|
|
0898a7bb57 | ||
|
|
fafd5234bd | ||
|
|
8cb10f76e4 | ||
|
|
f1d7f59e49 | ||
|
|
bc9a99387f | ||
|
|
5289d49f75 | ||
|
|
69e9f6d29d | ||
|
|
0b42437052 | ||
|
|
ae0f750770 | ||
|
|
9fe7e0d63d | ||
|
|
8935794e28 | ||
|
|
d44ff447bd | ||
|
|
798d3e2d54 | ||
|
|
e8f99c3326 | ||
|
|
1a5f380c00 | ||
|
|
b4827a98ca | ||
|
|
3ea5e4d4b2 | ||
|
|
5f77ac8d6f | ||
|
|
5d0cf3d919 | ||
|
|
4b1da0cf3c | ||
|
|
862ced3bd0 | ||
|
|
79b256a0fa | ||
|
|
0d6ff7d1b7 | ||
|
|
ecc5fe24a9 | ||
|
|
1fb2317bac | ||
|
|
6246eb9717 | ||
|
|
8f763c42b6 | ||
|
|
6472bda29e | ||
|
|
c0cad91cb6 | ||
|
|
1149dea4b2 | ||
|
|
6a6024e38f | ||
|
|
8901d11674 | ||
|
|
8b7f7cbc30 | ||
|
|
b6d0bdfa2d | ||
|
|
44896bcd51 | ||
|
|
bdf2b2d5c4 | ||
|
|
035726f650 | ||
|
|
1abb3cd566 | ||
|
|
f7772f00c4 | ||
|
|
216b5341ae | ||
|
|
eeeef9ca86 | ||
|
|
cc9293b386 | ||
|
|
efe43077bc | ||
|
|
949c7726d1 | ||
|
|
0b7bda291c | ||
|
|
872cf0d726 | ||
|
|
af09223dd5 | ||
|
|
7d62f103e4 | ||
|
|
9e85d37fb9 | ||
|
|
8dee06f83a | ||
|
|
82fe4aa6c0 | ||
|
|
50c169e0a3 | ||
|
|
7364525bf5 | ||
|
|
54910fdb76 | ||
|
|
332a3c4cbf | ||
|
|
ac41c41809 | ||
|
|
96a9df04ed | ||
|
|
b7cc4158d5 | ||
|
|
2bbe6269cd | ||
|
|
eb54189683 | ||
|
|
e8e59306fc | ||
|
|
8af3fe3b4a | ||
|
|
3103247e8f | ||
|
|
1629a7d280 | ||
|
|
b5a5169372 | ||
|
|
4b4bfae4f4 | ||
|
|
d5639e6e95 | ||
|
|
9e67f74ca3 | ||
|
|
e3ddfbf2b8 | ||
|
|
1ea78c7ae7 | ||
|
|
e7af3bf55d | ||
|
|
e52cec9cdf | ||
|
|
5bb48b51a0 | ||
|
|
d2e1b35eee | ||
|
|
ef204b0adf | ||
|
|
f742434043 | ||
|
|
d3b34ce323 | ||
|
|
89c2f4f2ff | ||
|
|
5a0f23e6d6 | ||
|
|
5e05e8b62b | ||
|
|
1f7273af23 | ||
|
|
2b8302bced | ||
|
|
1b94462410 | ||
|
|
120bb443fe | ||
|
|
6fc3c03c4b | ||
|
|
46b79c7c61 | ||
|
|
4782d8aa1f | ||
|
|
fe4e305410 | ||
|
|
040c1fc302 | ||
|
|
5edea5a8dc | ||
|
|
d2b65537f6 | ||
|
|
1183f68e19 | ||
|
|
da6fe01eca | ||
|
|
c27cea6f30 | ||
|
|
cd0532b4d6 | ||
|
|
c9de6c003b | ||
|
|
418621a9ff | ||
|
|
f871724ae6 | ||
|
|
def68ddc8f | ||
|
|
a31db3df9c | ||
|
|
64217a8a5b | ||
|
|
79079b54ea | ||
|
|
77a7619690 | ||
|
|
9f2d7adb8e | ||
|
|
07dd9c6bc8 | ||
|
|
45939171ea | ||
|
|
049849264e | ||
|
|
7e0d48c2a1 | ||
|
|
ad1468f66f | ||
|
|
058bcddc53 | ||
|
|
8288de0c84 | ||
|
|
1da2afd450 | ||
|
|
03de51747e | ||
|
|
3d698cd7c1 | ||
|
|
a48cc245e7 | ||
|
|
9ed3a8ee05 | ||
|
|
64daf1310d | ||
|
|
e5ba0d9d9c | ||
|
|
50e4e9d58d | ||
|
|
03b9db5e0a | ||
|
|
043cb2ea44 | ||
|
|
a62d70fbd5 | ||
|
|
053e80a08e | ||
|
|
b726dcc770 | ||
|
|
9df133ed8c | ||
|
|
50dd7b00c3 | ||
|
|
ccbd2c924b | ||
|
|
52d5c3beeb | ||
|
|
c43416891e | ||
|
|
56a573de86 | ||
|
|
e7fff2529c | ||
|
|
78867647d1 | ||
|
|
09f32d4f84 | ||
|
|
6f0f70bd92 | ||
|
|
6df15ddf6e | ||
|
|
922c0887f1 | ||
|
|
d7c9243880 | ||
|
|
f42a595aba | ||
|
|
797722ec12 | ||
|
|
bb4bf23c5c | ||
|
|
f3aacbd253 | ||
|
|
106fce26b5 | ||
|
|
caf208b0a4 | ||
|
|
13b9a8bc9a | ||
|
|
14ce230683 | ||
|
|
f31fbc10f6 | ||
|
|
be404068ff | ||
|
|
5671ec5f58 | ||
|
|
da3b0bf7c8 | ||
|
|
90ade3225f | ||
|
|
4928d1d490 | ||
|
|
9c52eb9d6f | ||
|
|
0a58cb2877 | ||
|
|
7581830e70 | ||
|
|
d468866746 | ||
|
|
999e170f1d | ||
|
|
7513bfb13a | ||
|
|
1f27002b84 | ||
|
|
669bfe763a | ||
|
|
860370a845 | ||
|
|
196761a40a | ||
|
|
26d5444919 | ||
|
|
e05c41828c | ||
|
|
c4cce58464 | ||
|
|
f7e6d4e724 | ||
|
|
d02e992265 | ||
|
|
3e13936e08 | ||
|
|
a3dfcd5a95 | ||
|
|
ce928dc6c8 | ||
|
|
1dea988cd6 | ||
|
|
74bb6f0012 | ||
|
|
79888d3bde | ||
|
|
4e1d3e45a3 | ||
|
|
682db77401 | ||
|
|
6faed08d9d | ||
|
|
62b200a4be | ||
|
|
f7bab5fdc0 | ||
|
|
5ff0ac2816 | ||
|
|
7c1889cd70 | ||
|
|
5669cc0002 | ||
|
|
d2ea5dd8b7 | ||
|
|
e0381b5920 | ||
|
|
dac3978983 | ||
|
|
7074cc28b8 | ||
|
|
327b6ad097 | ||
|
|
1dc837527f | ||
|
|
b1dd3c4866 | ||
|
|
624fb8da21 | ||
|
|
1ff405edd8 | ||
|
|
031e97ef91 | ||
|
|
3df0a9f132 | ||
|
|
1e79ab78dd | ||
|
|
1e48afeb8f | ||
|
|
b8ad1883f5 | ||
|
|
582fd24d78 | ||
|
|
a0963f8036 | ||
|
|
7d002474d7 | ||
|
|
ef77d7c608 | ||
|
|
63f6d0c036 | ||
|
|
aa5001f661 | ||
|
|
b01ea26719 | ||
|
|
c1a6229c2c | ||
|
|
4c9ec88be5 | ||
|
|
9011271a01 | ||
|
|
777ec0b36c | ||
|
|
795e4da8b8 | ||
|
|
79e59d5460 | ||
|
|
ba4c3e5bc4 | ||
|
|
88f2a66a51 | ||
|
|
bb081ca764 | ||
|
|
a9049b4a82 | ||
|
|
ae352a5d8c | ||
|
|
e2ad503bda | ||
|
|
2657060aa2 | ||
|
|
2724f3888a | ||
|
|
dc953ea680 | ||
|
|
08f8472db3 | ||
|
|
3f5e52f774 | ||
|
|
2e05ac0c90 | ||
|
|
40c5cd4b4b | ||
|
|
18f8c3d00a | ||
|
|
074fbf6f25 | ||
|
|
a482f20ba3 | ||
|
|
c36349f460 | ||
|
|
485f6d5386 | ||
|
|
778ca8e6f9 | ||
|
|
b64c6a3ac7 | ||
|
|
f76196937a | ||
|
|
ece93e5eef | ||
|
|
37bb89dac3 | ||
|
|
7d9aa97f96 | ||
|
|
ca31e5258f | ||
|
|
4912205adb | ||
|
|
9440dcf9de | ||
|
|
0aed47737c | ||
|
|
6e076472b8 | ||
|
|
3e15ae3211 | ||
|
|
26cb209af2 | ||
|
|
76f7726c47 | ||
|
|
9763fa9997 | ||
|
|
7be474bd83 | ||
|
|
30b3478611 | ||
|
|
f77ce209e0 | ||
|
|
a61356d018 | ||
|
|
2dc848506c | ||
|
|
9125e3c0c6 | ||
|
|
86dd9d87dd | ||
|
|
da3e00823f | ||
|
|
f3be2b3e68 | ||
|
|
988176e073 | ||
|
|
5d128adee1 | ||
|
|
71d4c552af | ||
|
|
d4ab607d0d | ||
|
|
ea307c8d94 | ||
|
|
7b4a0f20b2 | ||
|
|
3b93b5dde4 | ||
|
|
7ddb916a18 | ||
|
|
faba40554a | ||
|
|
c12752cf53 | ||
|
|
ca105692cf | ||
|
|
ce6f8ed1bc | ||
|
|
83748d78f8 | ||
|
|
72af7e4177 | ||
|
|
1767f91047 | ||
|
|
1759ddf247 | ||
|
|
f9643448a4 | ||
|
|
91f0b0e28f | ||
|
|
8d2af5cc61 | ||
|
|
eda4619a4f | ||
|
|
e849ca3372 | ||
|
|
630e446989 | ||
|
|
44248d9ab0 | ||
|
|
c87b2c02fa | ||
|
|
6e80371535 | ||
|
|
b4a350259d | ||
|
|
914fb36173 | ||
|
|
b882ac9e06 | ||
|
|
b8da166ab1 | ||
|
|
ca437a6504 | ||
|
|
72a31aed76 | ||
|
|
59e117738d | ||
|
|
75598ea2a1 | ||
|
|
e873816160 | ||
|
|
23626755d7 | ||
|
|
97af7e677b | ||
|
|
f9f7f74efb | ||
|
|
de482262e1 | ||
|
|
1b39c829ac | ||
|
|
fb09fb4472 | ||
|
|
12f9b1416f | ||
|
|
4dad7064cc | ||
|
|
628abc412e | ||
|
|
84a899c38a | ||
|
|
21d4e46bf1 | ||
|
|
c603691a98 | ||
|
|
efbbc5c6bc | ||
|
|
8dcc148a22 | ||
|
|
becd4cc3c0 | ||
|
|
e0ea2b75a1 | ||
|
|
a09bb5d4d8 | ||
|
|
7cd17d3a73 | ||
|
|
8a59a4404b | ||
|
|
5724fa534a | ||
|
|
e7210dd249 | ||
|
|
7d39cc75b2 | ||
|
|
b5066f1d8e | ||
|
|
266d8bf0d5 | ||
|
|
da8eac5538 | ||
|
|
67bbeb195b | ||
|
|
92183de29e | ||
|
|
8dae54ab8c | ||
|
|
62a31c27e1 | ||
|
|
dd29ee7288 | ||
|
|
fe64f2f4c9 | ||
|
|
03ea4a884a | ||
|
|
d3c7cbeea7 | ||
|
|
f0a1544ebd | ||
|
|
077f113618 | ||
|
|
0c6cbe7746 | ||
|
|
3bdfb2875f | ||
|
|
cc7ea736bb | ||
|
|
9dfe1bbadf | ||
|
|
1fd89b4f46 | ||
|
|
307d051ec2 | ||
|
|
3a668011fa | ||
|
|
14c8b80494 | ||
|
|
10dde518bc | ||
|
|
1e1c90c92e | ||
|
|
4954791443 | ||
|
|
c471f4927a | ||
|
|
9eba98302e | ||
|
|
250fe740b2 | ||
|
|
70eda031dc | ||
|
|
86f296a898 | ||
|
|
71ff18318d | ||
|
|
46cce28758 | ||
|
|
5611d9a3ef | ||
|
|
40bec49de8 | ||
|
|
f99d5f74d4 | ||
|
|
30a066aa41 | ||
|
|
1dcc3363d0 | ||
|
|
c6948582e6 | ||
|
|
196c83d058 | ||
|
|
806bee9646 | ||
|
|
45a0378c01 | ||
|
|
afd669194a | ||
|
|
1494a3863d | ||
|
|
f5c55f066b | ||
|
|
bd8f198beb | ||
|
|
f05adb4f99 | ||
|
|
3ebb91c07a | ||
|
|
771e87ebeb | ||
|
|
2598ce1d4b | ||
|
|
e2f3b2b41f | ||
|
|
7ebb8343d1 | ||
|
|
42479a75af | ||
|
|
22c7110349 | ||
|
|
44ee28bb2e | ||
|
|
f172f20219 | ||
|
|
0f7003d939 | ||
|
|
d2d88fe64e | ||
|
|
fa2a385a0c | ||
|
|
bd9579983e | ||
|
|
66bd86b9b7 | ||
|
|
364bdcf532 | ||
|
|
ba7e098373 | ||
|
|
9f71c8d2b9 | ||
|
|
fce7cdcc0a | ||
|
|
4fb52ce2ab | ||
|
|
2915134007 | ||
|
|
2f893bf361 | ||
|
|
f815c5607c | ||
|
|
59d61f00a6 | ||
|
|
262ff24c5b | ||
|
|
1189c2fab7 | ||
|
|
3eb3de3edc | ||
|
|
94601b4dc9 | ||
|
|
9ca0073cd7 | ||
|
|
55e6366529 | ||
|
|
bd66162972 | ||
|
|
5cdfd41dca | ||
|
|
a95fd581fd | ||
|
|
fda9f4ea7a | ||
|
|
f876d8fdc8 | ||
|
|
4198bbae6c | ||
|
|
ade54b38c1 | ||
|
|
0dd2c869a8 | ||
|
|
ed85ea69bd | ||
|
|
953298de74 | ||
|
|
628404e114 | ||
|
|
5638a40007 | ||
|
|
d6005dc0eb | ||
|
|
b3a7acbdad | ||
|
|
88ae550b93 | ||
|
|
2c3f5be093 | ||
|
|
95a4ca6f8e | ||
|
|
23432dd909 | ||
|
|
148f601bcb | ||
|
|
43d891b8d6 | ||
|
|
2eee079d3a | ||
|
|
30a555b108 | ||
|
|
8be970e688 | ||
|
|
12bf851c7d | ||
|
|
c837c54c39 | ||
|
|
5874529f43 | ||
|
|
e290710f67 | ||
|
|
438abd6003 | ||
|
|
442f6cd854 | ||
|
|
c2b154acad | ||
|
|
fbd61fcd17 | ||
|
|
b1529f19ad | ||
|
|
134566ed49 | ||
|
|
8da93fd762 | ||
|
|
63209ef71e | ||
|
|
f63ec38aae | ||
|
|
f858c8e750 | ||
|
|
26f80087dd | ||
|
|
0ac402792b | ||
|
|
974c6510b8 | ||
|
|
41df63cdc4 | ||
|
|
4080e9b501 | ||
|
|
53da858c06 | ||
|
|
50c9ae863a | ||
|
|
ce20d1b482 | ||
|
|
fcf916d138 | ||
|
|
f3c87bde88 | ||
|
|
3f7136fc7d | ||
|
|
59f5f5c1af | ||
|
|
1956301b1c | ||
|
|
1fd0f31682 | ||
|
|
e6a1bd6566 | ||
|
|
609f3f4bfa | ||
|
|
9b42cd2214 | ||
|
|
2d90e1e8ee | ||
|
|
ddf25e14af | ||
|
|
48f1adad49 | ||
|
|
379d37a255 | ||
|
|
a59ac064d2 | ||
|
|
433d54fcec | ||
|
|
146722beb8 | ||
|
|
eb5e54e9fd | ||
|
|
99707a527d | ||
|
|
9ee7793782 | ||
|
|
bc410d8e4a | ||
|
|
7561f5aa32 | ||
|
|
2855b5b4d5 | ||
|
|
419cb9feb8 | ||
|
|
dbf6bb5f27 | ||
|
|
f601108c5d | ||
|
|
b77abdc5e1 | ||
|
|
2fac2f9f1f | ||
|
|
e4beaf4de9 | ||
|
|
d4f134c6c7 | ||
|
|
7ebed76d16 | ||
|
|
2b812b01e9 | ||
|
|
2f5d5034db | ||
|
|
a32947e7a7 | ||
|
|
2fdadd383a | ||
|
|
9a2dc3fe15 | ||
|
|
f0c3d3fc4d | ||
|
|
2488e0044d | ||
|
|
9c866fd49c | ||
|
|
6c270b6e26 | ||
|
|
ae1c4536e6 | ||
|
|
f5b22d94d9 | ||
|
|
3c87ff4eff | ||
|
|
0f7b2c45d7 | ||
|
|
a12d18146c | ||
|
|
119d5be1a4 | ||
|
|
fcdc0174d9 | ||
|
|
4f4df8f9cc | ||
|
|
c730271e09 | ||
|
|
ac0eedda91 | ||
|
|
e87635295a | ||
|
|
62a662054b | ||
|
|
dc183c0d82 | ||
|
|
08e039bea9 | ||
|
|
88d329c52a | ||
|
|
fd8a455aff | ||
|
|
ed4574bda9 | ||
|
|
c9ae54a8c8 | ||
|
|
6fb83b740b | ||
|
|
7f89113245 | ||
|
|
0ea0c48631 | ||
|
|
cec4cb48cb | ||
|
|
b211a14a66 | ||
|
|
a3d1455c83 | ||
|
|
1716de3b59 | ||
|
|
44d8b3e8f3 | ||
|
|
4f4bb40ea6 | ||
|
|
db826b3c87 | ||
|
|
be658e7d64 | ||
|
|
53f06f6a4e | ||
|
|
c8add47fe7 | ||
|
|
28cd827cea | ||
|
|
ffda2839e0 | ||
|
|
28208e8364 | ||
|
|
9b7a6934b3 | ||
|
|
15229bbdab | ||
|
|
63e6eea9ec | ||
|
|
50d5b9e8e7 | ||
|
|
cc872b0444 | ||
|
|
17b84e09c0 | ||
|
|
43f8bae267 | ||
|
|
b0fe963f8a | ||
|
|
0822a9296c | ||
|
|
d9fa02c53b | ||
|
|
c44ee71ad4 | ||
|
|
826d1660c9 | ||
|
|
291a8e4de0 | ||
|
|
f02ccca0e0 | ||
|
|
1e12a60b34 | ||
|
|
8430b04492 | ||
|
|
35b72420ad | ||
|
|
28ba142fd6 | ||
|
|
b39bcd5c61 | ||
|
|
1fd35f3824 | ||
|
|
e73937c2bd | ||
|
|
b51ad4fcea | ||
|
|
d1a7c7283f | ||
|
|
b641ecdc74 | ||
|
|
13f567ff4c | ||
|
|
771d4b5811 | ||
|
|
3c944e0351 | ||
|
|
e26af258d6 | ||
|
|
76e5ec6d45 | ||
|
|
27cd12e2d9 | ||
|
|
bfaf1c4f70 | ||
|
|
2d18d089ce | ||
|
|
9c7e40906d | ||
|
|
401f291c3b | ||
|
|
bea2ae5ff5 | ||
|
|
f49e4946f2 | ||
|
|
8ff74072f8 | ||
|
|
fcd5aea04e | ||
|
|
1c0da2967c | ||
|
|
1b78a42b80 | ||
|
|
79e73d2eff | ||
|
|
23299f88e9 | ||
|
|
ef744e45c1 | ||
|
|
660cc2f3d1 | ||
|
|
469ac116ef | ||
|
|
a86103479b | ||
|
|
d49e75bd3e | ||
|
|
f4718a9047 | ||
|
|
7d5fe4b66c | ||
|
|
845c80721f | ||
|
|
0e65db10d8 | ||
|
|
a9cc321981 | ||
|
|
6349214f00 | ||
|
|
96f821b841 | ||
|
|
964e3872c1 | ||
|
|
5dfa26ea8b | ||
|
|
dbf042b8ad | ||
|
|
014e06eefd | ||
|
|
39a2122dc0 | ||
|
|
fe6d8d62c5 | ||
|
|
570d27ffaa | ||
|
|
7b69aa1fda | ||
|
|
21e478dd59 | ||
|
|
d14fb36cb9 | ||
|
|
19a808642f | ||
|
|
e921ba0910 | ||
|
|
0f5a073d57 | ||
|
|
cb0bdd89c0 | ||
|
|
e89bf5d06b | ||
|
|
e82d2f37a1 | ||
|
|
65e955c622 | ||
|
|
e73f4c6b7e | ||
|
|
cf5cefb2d6 | ||
|
|
36ac764133 | ||
|
|
003e45d2f5 | ||
|
|
04e93317b8 | ||
|
|
f8dedb710b | ||
|
|
1c259f69f6 | ||
|
|
913f17ee3e | ||
|
|
6291c53966 | ||
|
|
267730bc00 | ||
|
|
d5db02a899 | ||
|
|
7ed8ee160d | ||
|
|
3dd33b65a0 | ||
|
|
b85048f616 | ||
|
|
0852f53455 | ||
|
|
10fa119ab3 | ||
|
|
b5404c6159 | ||
|
|
42d21c4bb6 | ||
|
|
cc13ae252a | ||
|
|
b97f844a3e | ||
|
|
1d6eb015c1 | ||
|
|
07a8ae8c3e | ||
|
|
f05a5e531e | ||
|
|
68586ec49a | ||
|
|
6cf75af0af | ||
|
|
304607ae5d | ||
|
|
e9f28855a2 | ||
|
|
66d7d5f312 | ||
|
|
59734f1069 | ||
|
|
2974a57943 | ||
|
|
fcdcd1c335 | ||
|
|
4a35f9fcdb | ||
|
|
674b14802e | ||
|
|
3e36affa69 | ||
|
|
97d7a8ad0c | ||
|
|
b89ba365d0 | ||
|
|
47ff388549 | ||
|
|
647ab9bf0f | ||
|
|
76431b4673 | ||
|
|
be0dd29e3a | ||
|
|
40fbce91ce | ||
|
|
33d287d2f0 | ||
|
|
9eb1cbc514 | ||
|
|
40b173118a | ||
|
|
8822c409e2 | ||
|
|
aa750c0819 | ||
|
|
d90d9d7330 | ||
|
|
a8db672ffb | ||
|
|
76b66ae26f | ||
|
|
a2790cfe8e | ||
|
|
624ae45ebb | ||
|
|
2756b82f57 | ||
|
|
52f41ab0d5 | ||
|
|
fbb767893e | ||
|
|
229f5ee48c | ||
|
|
96c7741ba0 | ||
|
|
517b7d0283 | ||
|
|
0c0231c3e8 | ||
|
|
a9559a5c87 | ||
|
|
814ee24c8d | ||
|
|
7876cddf4a | ||
|
|
e9051355a1 | ||
|
|
29316a528a | ||
|
|
036b53acf8 | ||
|
|
919463ff02 | ||
|
|
3f7ec3f3b8 | ||
|
|
19604214d7 | ||
|
|
f7add8d788 | ||
|
|
d97c230747 | ||
|
|
906a49049e | ||
|
|
c1a4bd0482 | ||
|
|
d0336fe16f | ||
|
|
61b4bbf74e | ||
|
|
384c2e13d7 | ||
|
|
198d237679 | ||
|
|
39315ca1e2 | ||
|
|
efb51eee96 | ||
|
|
fbbd16bd82 | ||
|
|
bd2c1eef53 | ||
|
|
d1395b15bb | ||
|
|
2d8ed5e274 | ||
|
|
6a5d8ba859 | ||
|
|
320e2a6536 | ||
|
|
3858118340 | ||
|
|
6420068569 | ||
|
|
95b147079f | ||
|
|
83757f1065 | ||
|
|
f2036b42e5 | ||
|
|
21b7d41845 | ||
|
|
91a404d033 | ||
|
|
d027cf969c | ||
|
|
c7f68a2ef9 | ||
|
|
78e55a05c1 | ||
|
|
ca71555d0b | ||
|
|
77fdac01ff | ||
|
|
8301fae01e | ||
|
|
e9161ad702 | ||
|
|
a0a139da1f | ||
|
|
8f13d1da91 | ||
|
|
d5fe9ce2c7 | ||
|
|
37acc17cf3 | ||
|
|
569ec5919c | ||
|
|
19719becf5 | ||
|
|
e64057b803 | ||
|
|
672667aa3e | ||
|
|
8a06b6067e | ||
|
|
2dcc52abd0 | ||
|
|
c831ad39c9 | ||
|
|
0cf78ea9ad | ||
|
|
3d51fbf354 | ||
|
|
e7a2c7cc3e | ||
|
|
708a078412 | ||
|
|
bbcc4b7b70 | ||
|
|
45bba0a3c5 | ||
|
|
d105e2690a | ||
|
|
32d3e497c3 | ||
|
|
30a5d1b486 | ||
|
|
6b3ea56add | ||
|
|
c3aefdb98e | ||
|
|
094939451d | ||
|
|
0e23f44b84 | ||
|
|
daecdd7c2b | ||
|
|
7c8df28d01 | ||
|
|
65917272a2 | ||
|
|
137fd80fdb | ||
|
|
98fbc61221 | ||
|
|
f80d15062b | ||
|
|
b1b0219f04 | ||
|
|
b1941c33f7 | ||
|
|
a15a7b607d | ||
|
|
d50283f5ee | ||
|
|
6508d3b872 | ||
|
|
65b8cef1b8 | ||
|
|
5d460e1e5e | ||
|
|
3d3e0be7bd | ||
|
|
c06c0b7133 | ||
|
|
91f6630907 | ||
|
|
60085cf679 | ||
|
|
389480b8fc | ||
|
|
b5c4f78e9d | ||
|
|
59b0e2d70a | ||
|
|
39bd1a4628 | ||
|
|
1c1445c896 | ||
|
|
1e8ade2431 | ||
|
|
a990fbc3eb | ||
|
|
e5574e7fe5 | ||
|
|
6c8a924fad | ||
|
|
64706257ca | ||
|
|
6183d92315 | ||
|
|
31823a7405 | ||
|
|
85ddd623f6 | ||
|
|
9212dda9c3 | ||
|
|
93d7b37c8d | ||
|
|
8470bcd71d | ||
|
|
3aab37611a | ||
|
|
8fbcc36331 | ||
|
|
dadb646252 | ||
|
|
0227b93409 | ||
|
|
b0ec0821d5 | ||
|
|
13a7806cac | ||
|
|
41c76fb748 | ||
|
|
ac0c3b9f92 | ||
|
|
1be0ff8da7 | ||
|
|
2169b5109f | ||
|
|
4a2292a53c | ||
|
|
7df4b736cf | ||
|
|
e47ad846c4 | ||
|
|
8f68ac2129 | ||
|
|
1ea2825a54 | ||
|
|
19146d61b1 | ||
|
|
e541b809ce | ||
|
|
6ca08c6519 | ||
|
|
b43540820b | ||
|
|
3d57da71eb | ||
|
|
0130fd3666 | ||
|
|
395afc4a8d | ||
|
|
31e201ca52 | ||
|
|
0abd7ad6be | ||
|
|
b3522c48d9 | ||
|
|
0fc58a7986 | ||
|
|
54241d8ab9 | ||
|
|
355f1615ab | ||
|
|
113252b0ae | ||
|
|
1cd7d14029 | ||
|
|
87c2fb6a4a | ||
|
|
9912998bb7 | ||
|
|
e223d3d8de | ||
|
|
ec31fc4cc7 | ||
|
|
3ce2b9b79a | ||
|
|
a79182e50d | ||
|
|
6f4c595dde | ||
|
|
0eb3090ad6 | ||
|
|
6ea25bd259 | ||
|
|
fe5f087f9c | ||
|
|
79299be3b2 | ||
|
|
4c9b620bd0 | ||
|
|
a7508a5dfd | ||
|
|
1a3d765c4c | ||
|
|
4058c71ca0 | ||
|
|
3fc22a6010 | ||
|
|
a9fe0b8000 | ||
|
|
5af7b0235e | ||
|
|
bf946200e9 | ||
|
|
890cc87724 | ||
|
|
8eb0b0f4ca | ||
|
|
e6a8dc0bcf | ||
|
|
02c497fad6 | ||
|
|
d0ab747479 | ||
|
|
f94d0be2c9 | ||
|
|
9fd9fd6816 | ||
|
|
b8717d750a | ||
|
|
8ad01fe32f | ||
|
|
fdb543fa7d | ||
|
|
52b5a6410c | ||
|
|
0034cfef5c | ||
|
|
78b62be96f | ||
|
|
1f5ccab1ce | ||
|
|
46be280c92 | ||
|
|
2a5763a771 | ||
|
|
370cec098b | ||
|
|
49a2f0191f | ||
|
|
fabdda0492 | ||
|
|
6fc3290a05 | ||
|
|
66e6369c28 | ||
|
|
0f0da9c32a | ||
|
|
0a69c1a02d | ||
|
|
feaf98bd01 | ||
|
|
0fe9c15ce8 | ||
|
|
f528e12c83 | ||
|
|
8ca9f93ccf | ||
|
|
73d8064837 | ||
|
|
5b1f60b7eb | ||
|
|
2e1344f611 | ||
|
|
5b9996b16f | ||
|
|
6fdc1791e4 | ||
|
|
fd4f37b5c3 | ||
|
|
d76e8887e5 | ||
|
|
eb9134685a | ||
|
|
d929b84786 | ||
|
|
8ef3297b11 | ||
|
|
27c7aeb117 | ||
|
|
c9714600e8 | ||
|
|
665fdded14 | ||
|
|
814a0ea36f | ||
|
|
71e018a3dd | ||
|
|
efb26f8b60 | ||
|
|
d9eb6e2682 | ||
|
|
b74107f2ba | ||
|
|
0cd91a10c6 | ||
|
|
f062e1dcda | ||
|
|
9f5397a2d4 | ||
|
|
0164abbd4a | ||
|
|
e92af63636 | ||
|
|
94501c683b | ||
|
|
047c3cf880 | ||
|
|
47d7d87c82 | ||
|
|
5f53d50492 | ||
|
|
5f71f87496 | ||
|
|
c6cb90e8ca | ||
|
|
fb156bcaac | ||
|
|
75ba2196ba | ||
|
|
4cb50b15e4 | ||
|
|
ca5cbe4d44 | ||
|
|
df050472a1 | ||
|
|
c173ebf5b9 | ||
|
|
434582b5f5 | ||
|
|
cf6be928a3 | ||
|
|
c907c55144 | ||
|
|
ee433ab909 | ||
|
|
bf69923b6d | ||
|
|
64782a433e | ||
|
|
44edb49a6e | ||
|
|
1a6d269063 | ||
|
|
b64953ebdb | ||
|
|
deaa2bcb15 | ||
|
|
c166c57c5d | ||
|
|
6b77e4ee4a | ||
|
|
e5534f060d | ||
|
|
466e0be560 | ||
|
|
810adab957 | ||
|
|
83a3c9fc8d | ||
|
|
5e95019b3f | ||
|
|
8e7f278094 | ||
|
|
83a895a463 | ||
|
|
59ae1e1599 | ||
|
|
77a82e9d51 | ||
|
|
bd79c2e8dc | ||
|
|
23bcc19180 | ||
|
|
282f08df36 | ||
|
|
d647a96ed5 | ||
|
|
1b64ea3210 | ||
|
|
9b32e99eb8 | ||
|
|
79e696d8a7 | ||
|
|
a3ea19be8e | ||
|
|
e0015a52e5 | ||
|
|
aea4661be5 | ||
|
|
80377e4716 | ||
|
|
c3d54f3c2e | ||
|
|
c7d367a791 | ||
|
|
ba4253668d | ||
|
|
1ce5c69cd2 | ||
|
|
205d731d7b | ||
|
|
3e875cc593 | ||
|
|
de2cfc7e17 | ||
|
|
e72cab81c1 | ||
|
|
844a2db83a | ||
|
|
529ba45cc7 | ||
|
|
09aabce3cd | ||
|
|
eb2bfd3848 | ||
|
|
adb5c8fe06 | ||
|
|
d914d40b2e | ||
|
|
66c7672a0c | ||
|
|
983379d334 | ||
|
|
fd72a09d1e | ||
|
|
0ddf7c05c8 | ||
|
|
818134247d | ||
|
|
ed020e18a0 | ||
|
|
9e5acb84c0 | ||
|
|
5e45ae1584 | ||
|
|
86b101c410 | ||
|
|
96ca7262e4 | ||
|
|
0a31edecb6 | ||
|
|
6ac6d142c0 | ||
|
|
5bb9900220 | ||
|
|
f99a200db0 | ||
|
|
24e73cdd8b | ||
|
|
be8f589c32 | ||
|
|
3074ae99ea | ||
|
|
873fe41ab3 | ||
|
|
029de4ac86 | ||
|
|
5f21f190b9 | ||
|
|
dab78c8a63 | ||
|
|
0d1230a959 | ||
|
|
c507e5f562 | ||
|
|
63e353ad6a | ||
|
|
7194dfa43c | ||
|
|
e425f1df87 | ||
|
|
3f4613feb0 | ||
|
|
033c21754b | ||
|
|
c89c35c6b3 | ||
|
|
1dbfea54bc | ||
|
|
0af8784707 | ||
|
|
2ca5766f56 | ||
|
|
a0b842204c | ||
|
|
0a26050b47 | ||
|
|
c50ab9872d | ||
|
|
fa6893fda9 | ||
|
|
710abded64 | ||
|
|
1c38db1fc7 | ||
|
|
339e1b5dcf | ||
|
|
7113ed73d4 | ||
|
|
3dd1daacdc | ||
|
|
bad06bb634 | ||
|
|
e18e81f5eb | ||
|
|
67a446234c | ||
|
|
f905b27b00 | ||
|
|
e36ee0b4f1 | ||
|
|
3c13229145 | ||
|
|
cea24c2cf9 | ||
|
|
64017cf874 | ||
|
|
b3bce8a1ba | ||
|
|
3b0cef2ec8 | ||
|
|
07cbae4019 | ||
|
|
b42202ea1c | ||
|
|
8347dcd671 | ||
|
|
dcb5285797 | ||
|
|
a9cd647075 | ||
|
|
f0cd730fbb | ||
|
|
2afbd7ba7f | ||
|
|
55ff0c0dee | ||
|
|
6b7aaeca45 | ||
|
|
1f3e1720a3 | ||
|
|
b7f2d0366b | ||
|
|
6bd0979b4a | ||
|
|
986abc1e45 | ||
|
|
61dac10bb9 | ||
|
|
b5385f2560 | ||
|
|
92e43d9e77 | ||
|
|
325408d0e3 | ||
|
|
eeb667954f | ||
|
|
8aa1062e06 | ||
|
|
7e0a8f235e | ||
|
|
44bbc106a9 | ||
|
|
e6be849eb2 | ||
|
|
092f27495a | ||
|
|
7849f91d80 | ||
|
|
5f769da74d | ||
|
|
963c034b48 | ||
|
|
f15e47bb67 | ||
|
|
7995d56a85 | ||
|
|
30aed94aa8 | ||
|
|
3b1d705473 | ||
|
|
f43ba728e3 | ||
|
|
b662362570 | ||
|
|
99ece6fc35 | ||
|
|
8287659584 | ||
|
|
cf95ab9a28 | ||
|
|
b907c74386 | ||
|
|
a68fb4fb8f | ||
|
|
945fccd211 | ||
|
|
12b84307ac | ||
|
|
6843741d9e | ||
|
|
945edb253b | ||
|
|
cbc82cd3c1 | ||
|
|
29ee239987 | ||
|
|
3f7e107d09 | ||
|
|
22c0d79e2d | ||
|
|
e174e5254d | ||
|
|
de5bcb8b9c | ||
|
|
98666186ee | ||
|
|
941d3c6648 | ||
|
|
5c518eda0a | ||
|
|
df72eee201 | ||
|
|
131113b065 | ||
|
|
e85310c0a9 | ||
|
|
cd17b46b55 | ||
|
|
d0d92c7697 | ||
|
|
89a9b4e6d5 | ||
|
|
592a2ff196 | ||
|
|
17ed90c790 | ||
|
|
9e0b335669 | ||
|
|
194c554357 | ||
|
|
c65790b53d | ||
|
|
2f37c0caaf | ||
|
|
86a39e3aea | ||
|
|
326b1ca8c9 | ||
|
|
72fe770974 | ||
|
|
db8c398fa3 | ||
|
|
861bcc38be | ||
|
|
cd3874ffb7 | ||
|
|
10fe88a2cf | ||
|
|
1a38bfb76d | ||
|
|
beaebb7dc7 | ||
|
|
6d5d054c30 | ||
|
|
2344155379 | ||
|
|
48347d4d86 | ||
|
|
61deaaddb7 | ||
|
|
0046e9c469 | ||
|
|
733145d132 | ||
|
|
f285d80d0e | ||
|
|
0ffccbd3ee | ||
|
|
1fc120de2d | ||
|
|
d5e443e8e3 | ||
|
|
a3c84296bf | ||
|
|
cc039d1f9b | ||
|
|
2484ec9c11 | ||
|
|
5f9de1f034 | ||
|
|
66eaaf9cbb | ||
|
|
87ac193b5e | ||
|
|
11e57edbb3 | ||
|
|
4f2c42ea47 | ||
|
|
820f3d5cbb | ||
|
|
081598d989 | ||
|
|
09f268befc | ||
|
|
4bc974c83c | ||
|
|
63da8f48da | ||
|
|
32d6a17240 | ||
|
|
84d869a3a0 | ||
|
|
a1c6619401 | ||
|
|
3524f6baa9 | ||
|
|
ac5cbc1d2c | ||
|
|
a045313e08 | ||
|
|
9bd2dc3050 | ||
|
|
02fef3136f | ||
|
|
8fe0e00cd9 | ||
|
|
f7f19bbc02 | ||
|
|
95ae806e09 | ||
|
|
d8a6f173c3 | ||
|
|
431f1aa766 | ||
|
|
379dcf0972 | ||
|
|
0d25d113c9 | ||
|
|
7c70913e8d | ||
|
|
1c5858c515 | ||
|
|
c3767bb3b3 | ||
|
|
b92d27ee7f | ||
|
|
6eff139c40 | ||
|
|
07462303ab | ||
|
|
d12f81b44e | ||
|
|
600112780c | ||
|
|
4c73c8889f | ||
|
|
68d5c2bc10 | ||
|
|
7db1fee877 | ||
|
|
8f786e3fd9 | ||
|
|
1c704e11f2 | ||
|
|
e0dd1cb29d | ||
|
|
827837b0b9 | ||
|
|
e83ef9858b | ||
|
|
504d506575 | ||
|
|
823b436b53 | ||
|
|
212327d746 | ||
|
|
cc138fc70e | ||
|
|
d953712377 | ||
|
|
69ac0036e6 | ||
|
|
975a5315b0 | ||
|
|
8f734b11e3 | ||
|
|
17b4cabc71 | ||
|
|
75db4faf69 | ||
|
|
e1f5601d4b | ||
|
|
b60ecdaa24 | ||
|
|
9fb9962ce7 | ||
|
|
25c93c678d | ||
|
|
b8baef7b8f | ||
|
|
235a47bb80 | ||
|
|
c107eed890 | ||
|
|
abddea060e | ||
|
|
3e40369fd2 | ||
|
|
0f0fda1660 | ||
|
|
bd2170a99c | ||
|
|
c039e5bed0 | ||
|
|
527c025a0c | ||
|
|
53cded77f1 | ||
|
|
4a4dc676fc | ||
|
|
c61bfbdd4c | ||
|
|
e38d9d5f22 | ||
|
|
97f060d38d | ||
|
|
357b8fa98f | ||
|
|
8754d766e2 | ||
|
|
2388c3ee9a | ||
|
|
61890cb9de | ||
|
|
5a0d0bb299 | ||
|
|
2746b1bd38 | ||
|
|
e09aac6450 | ||
|
|
19a6368377 | ||
|
|
e6122122e9 | ||
|
|
492614ebc7 | ||
|
|
d31f0ed39b | ||
|
|
b505c295d2 | ||
|
|
0b9d7edd47 | ||
|
|
e9fbb608a8 | ||
|
|
6ba05c94ea | ||
|
|
07fec6d00e | ||
|
|
a69b985086 | ||
|
|
471733fe03 | ||
|
|
0d3a193ab5 | ||
|
|
ab9fa291a8 | ||
|
|
cadc74eeec | ||
|
|
fc3a57b5e2 | ||
|
|
7ff07e1454 | ||
|
|
3e779bca8d | ||
|
|
0f1abcb10c | ||
|
|
55538a3695 | ||
|
|
878a15aff4 | ||
|
|
60e33f5d8c | ||
|
|
b422692746 | ||
|
|
f34be1896a | ||
|
|
c350cdba43 | ||
|
|
1a933eaa73 | ||
|
|
bd46e9c5b0 | ||
|
|
09b7ae21bc | ||
|
|
acfc961909 | ||
|
|
f502f75e1f | ||
|
|
ff97ef7b94 | ||
|
|
a2c780b085 | ||
|
|
b99305c909 | ||
|
|
d84dfc23e7 | ||
|
|
9d8fd3594e | ||
|
|
e68dbeb7eb | ||
|
|
32ddf0c296 | ||
|
|
c453bfeb32 | ||
|
|
f6ca450d36 | ||
|
|
d5f617ec92 | ||
|
|
6d104bfa91 | ||
|
|
e583cc2519 | ||
|
|
0d208b7957 | ||
|
|
43e5c042a2 | ||
|
|
39844ffef9 | ||
|
|
f5c8aac97d | ||
|
|
b6447ebdbb | ||
|
|
466fc4227e | ||
|
|
c034c88be4 | ||
|
|
72830efc45 | ||
|
|
c98eddc185 | ||
|
|
3b2353b5ae | ||
|
|
3f567c952c | ||
|
|
4f7f6a073c | ||
|
|
0e008cc15f | ||
|
|
1ad9c6faac | ||
|
|
06fe726ee7 | ||
|
|
1b6e46973e | ||
|
|
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 | ||
|
|
af8d362caa | ||
|
|
c64fb87b2b |
@@ -1,73 +0,0 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.11
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Ensure go.mod file is up to date
|
||||
command: |
|
||||
export GO111MODULE=on
|
||||
rm go.sum
|
||||
mv go.mod /tmp/
|
||||
go mod init
|
||||
export GO111MODULE=auto
|
||||
|
||||
if [ $(diff /tmp/go.mod go.mod|wc -l) -gt 0 ]; then
|
||||
diff /tmp/go.mod go.mod
|
||||
exit 1;
|
||||
fi
|
||||
- run:
|
||||
name: Run gofmt -s
|
||||
command: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1;
|
||||
fi
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pkg-cache-{{ checksum "Gopkg.lock" }}-v3
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
./test.sh
|
||||
- run:
|
||||
name: Compile project on every platform
|
||||
command: |
|
||||
go get github.com/mitchellh/gox
|
||||
gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
|
||||
- run:
|
||||
name: Push on codecov result
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
- save_cache:
|
||||
key: pkg-cache-{{ checksum "Gopkg.lock" }}-v3
|
||||
paths:
|
||||
- ~/.cache/go-build
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run gorelease
|
||||
command: |
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build
|
||||
release:
|
||||
jobs:
|
||||
- release:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jesseduffield]
|
||||
custom: ['https://donorbox.org/lazygit']
|
||||
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/discussion.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Discussion
|
||||
about: Begin a discussion
|
||||
title: ''
|
||||
labels: discussion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Topic**
|
||||
A clear and concise description of what you want to discuss
|
||||
|
||||
**Your thoughts**
|
||||
What you have to say about the topic
|
||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
28
.github/workflows/automerge.yml
vendored
Normal file
28
.github/workflows/automerge.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: rebase
|
||||
54
.github/workflows/cd.yml
vendored
Normal file
54
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Unshallow repo
|
||||
run: git fetch --prune --unshallow
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
homebrew:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Bump Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
ppa:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout PPA repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dawidd6/lazygit-debian
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Setup git
|
||||
uses: dawidd6/action-git-user-config@v1
|
||||
- name: Update PPA repo
|
||||
run: |
|
||||
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
|
||||
sudo apt update
|
||||
sudo apt install -y git-buildpackage
|
||||
git fetch --tags https://github.com/$GITHUB_REPOSITORY
|
||||
gbp import-ref -u "$version"
|
||||
gbp dch -D xenial -N "$version"-1
|
||||
git add debian/changelog
|
||||
git commit -m "d/changelog: dch $version"
|
||||
gbp tag
|
||||
git push --tags origin master
|
||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
- name: Test code
|
||||
run: |
|
||||
./test.sh
|
||||
- name: Build binaries
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
args: --skip-publish --snapshot
|
||||
14
.github/workflows/lint.yml
vendored
Normal file
14
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Lint
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: latest
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -23,4 +23,12 @@ lazygit
|
||||
!.gitignore
|
||||
!.goreleaser.yml
|
||||
!.circleci/
|
||||
!.github/
|
||||
!.github/
|
||||
|
||||
test/git_server/data
|
||||
test/integration/*/actual/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too space space
|
||||
test/integration/*/expected/.git_keep/hooks/
|
||||
!.git_keep/
|
||||
lazygit.exe
|
||||
|
||||
@@ -17,16 +17,16 @@ builds:
|
||||
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
|
||||
archives:
|
||||
- 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:
|
||||
@@ -38,27 +38,26 @@ changelog:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^bump'
|
||||
brew:
|
||||
# Reporitory to push the tap to.
|
||||
github:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
brews:
|
||||
-
|
||||
# Repository to push the tap to.
|
||||
tap:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: 'https://github.com/jesseduffield/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'
|
||||
# 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
|
||||
# # Packages your package depends on.
|
||||
# dependencies:
|
||||
# - git
|
||||
# - zsh
|
||||
# # Packages that conflict with your package.
|
||||
# conflicts:
|
||||
# - svn
|
||||
# - bash
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -1,13 +1,17 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:alpine
|
||||
FROM golang:1.14-alpine3.11
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY ./ .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build
|
||||
|
||||
FROM alpine:3.11
|
||||
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
|
||||
|
||||
ADD . /go/src/github.com/jesseduffield/lazygit
|
||||
|
||||
RUN go install github.com/jesseduffield/lazygit
|
||||
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit
|
||||
ENTRYPOINT [ "lazygit" ]
|
||||
|
||||
634
Gopkg.lock
generated
634
Gopkg.lock
generated
@@ -1,634 +0,0 @@
|
||||
# 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: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:66bb9b4a5abb704642fccba52a84a7f7feef2d9623f87b700e52a6695044723f"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "03e26ff3f1de2c1bc2205113c3aba661312eee00"
|
||||
|
||||
[[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/golang-collections/collections/stack",
|
||||
"github.com/heroku/rollrus",
|
||||
"github.com/jesseduffield/go-getter",
|
||||
"github.com/jesseduffield/gocui",
|
||||
"github.com/kardianos/osext",
|
||||
"github.com/mgutz/str",
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n",
|
||||
"github.com/shibukawa/configdir",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/spf13/viper",
|
||||
"github.com/spkg/bom",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/tcnksm/go-gitconfig",
|
||||
"golang.org/x/text/language",
|
||||
"gopkg.in/src-d/go-git.v4",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing",
|
||||
"gopkg.in/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
46
Gopkg.toml
46
Gopkg.toml
@@ -1,46 +0,0 @@
|
||||
# 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]]
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spkg/bom"
|
||||
284
README.md
284
README.md
@@ -1,36 +1,88 @@
|
||||
# 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) []()
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/oYB7Cj8.png">
|
||||
</p>
|
||||
|
||||
 [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/github.com/jesseduffield/lazygit) []() [](https://www.tickgit.com/browse?repo=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!
|
||||
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
|
||||
|
||||
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
|
||||
|
||||

|
||||

|
||||
|
||||
* [Installation](https://github.com/jesseduffield/lazygit#installation)
|
||||
* [Usage](https://github.com/jesseduffield/lazygit#usage),
|
||||
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
|
||||
* [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
|
||||
* [Contributing](https://github.com/jesseduffield/lazygit#contributing)
|
||||
* [Video Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
|
||||
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
## Table of contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Fedora and CentOS 7](#fedora-and-centos-7)
|
||||
- [Solus Linux](#solus-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
- [Chocolatey (Windows)](#chocolatey-windows)
|
||||
- [Manual](#manual)
|
||||
- [Usage](#usage)
|
||||
- [Keybindings](#keybindings)
|
||||
- [Changing directory on exit](#changing-directory-on-exit)
|
||||
- [Undo/Redo](#undoredo)
|
||||
- [Configuration](#configuration)
|
||||
- [Custom pagers](#configuration)
|
||||
- [Custom commands](#configuration)
|
||||
- [Tutorials](#tutorials)
|
||||
- [Cool Features](#cool-features)
|
||||
- [Contributing](#contributing)
|
||||
- [Donate](#donate)
|
||||
- [Alternatives](#alternatives)
|
||||
|
||||
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
|
||||
|
||||
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
|
||||
|
||||
## Installation
|
||||
|
||||
### Binary Releases
|
||||
|
||||
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
```sh
|
||||
brew tap jesseduffield/lazygit
|
||||
|
||||
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
|
||||
|
||||
Tap:
|
||||
|
||||
```
|
||||
brew install jesseduffield/lazygit/lazygit
|
||||
```
|
||||
|
||||
Core:
|
||||
|
||||
```
|
||||
brew install lazygit
|
||||
```
|
||||
|
||||
### MacPorts
|
||||
|
||||
Latest version built from github releases.
|
||||
Tap:
|
||||
|
||||
```
|
||||
sudo port 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.
|
||||
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||
|
||||
```sh
|
||||
sudo add-apt-repository ppa:lazygit-team/release
|
||||
@@ -38,17 +90,8 @@ 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
|
||||
@@ -57,86 +100,199 @@ They follow upstream latest releases
|
||||
sudo xbps-install -S lazygit
|
||||
```
|
||||
|
||||
### Scoop (Windows)
|
||||
|
||||
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
|
||||
|
||||
```sh
|
||||
# Add the extras bucket
|
||||
scoop bucket add extras
|
||||
|
||||
# Install lazygit
|
||||
scoop install lazygit
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
|
||||
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
|
||||
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
* Stable: https://aur.archlinux.org/packages/lazygit/
|
||||
* Development: https://aur.archlinux.org/packages/lazygit-git/
|
||||
- Stable: `sudo pacman -S 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
|
||||
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
|
||||
|
||||
### Binary Release (Windows/Linux/OSX)
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
|
||||
### Fedora and CentOS 7
|
||||
|
||||
Packages for Fedora and CentOS 7 are available via [Copr](https://copr.fedorainfracloud.org/coprs/atim/lazygit/) (Cool Other Package Repo).
|
||||
|
||||
```sh
|
||||
sudo dnf copr enable atim/lazygit -y
|
||||
sudo dnf install lazygit
|
||||
```
|
||||
|
||||
### Solus Linux
|
||||
|
||||
```sh
|
||||
sudo eopkg install lazygit
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
pkg install lazygit
|
||||
```
|
||||
|
||||
|
||||
### Conda
|
||||
|
||||
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
|
||||
|
||||
```sh
|
||||
conda install -c conda-forge lazygit
|
||||
```
|
||||
|
||||
### 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`
|
||||
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).
|
||||
|
||||
### Chocolatey (Windows)
|
||||
|
||||
You can install `lazygit` using [Chocolatey](https://chocolatey.org/):
|
||||
|
||||
```sh
|
||||
choco install lazygit
|
||||
```
|
||||
|
||||
### Manual
|
||||
|
||||
You'll need to [install Go](https://golang.org/doc/install)
|
||||
|
||||
```
|
||||
git clone https://github.com/jesseduffield/lazygit.git
|
||||
cd lazygit
|
||||
go install
|
||||
```
|
||||
|
||||
You can also use `go run main.go` to compile and run in one go (pun definitely intended)
|
||||
|
||||
## Usage
|
||||
Call `lazygit` in your terminal inside a git repository. If you want, you can
|
||||
|
||||
Call `lazygit` in your terminal inside a git repository.
|
||||
|
||||
```sh
|
||||
$ lazygit
|
||||
```
|
||||
|
||||
If you want, you can
|
||||
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
|
||||
whichever rc file you're using).
|
||||
|
||||
* Basic video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
|
||||
* List of keybindings
|
||||
[here](/docs/Keybindings.md).
|
||||
### Keybindings
|
||||
|
||||
You can check out the list of keybindings [here](/docs/keybindings).
|
||||
|
||||
### Changing Directory On Exit
|
||||
|
||||
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
|
||||
|
||||
```
|
||||
lg()
|
||||
{
|
||||
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
|
||||
|
||||
lazygit "$@"
|
||||
|
||||
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
|
||||
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
|
||||
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
|
||||
|
||||
### Undo/Redo
|
||||
|
||||
See the [docs](/docs/Undoing.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
Check out the [configuration docs](docs/Config.md).
|
||||
|
||||
### Custom Pagers
|
||||
|
||||
See the [docs](docs/Custom_Pagers.md)
|
||||
|
||||
### Custom Commands
|
||||
|
||||
If lazygit is missing a feature, there's a good chance you can implement it yourself with a custom command!
|
||||
|
||||
See the [docs](docs/Custom_Command_Keybindings.md)
|
||||
|
||||
## Tutorials
|
||||
|
||||
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
|
||||
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
|
||||
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
|
||||
|
||||
## Cool features
|
||||
* Adding files easily
|
||||
* Resolving merge conflicts
|
||||
* Easily check out recent branches
|
||||
* Scroll through logs/diffs of branches/commits/stash
|
||||
* Quick pushing/pulling
|
||||
* Squash down and rename commits
|
||||
|
||||
- Adding files easily
|
||||
- Resolving merge conflicts
|
||||
- Easily check out recent branches
|
||||
- Scroll through logs/diffs of branches/commits/stash
|
||||
- Quick pushing/pulling
|
||||
- Squash down and rename commits
|
||||
|
||||
### Resolving merge conflicts
|
||||

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

|
||||

|
||||
|
||||
## Milestones
|
||||
- [x] Easy Installation (homebrew, release binaries)
|
||||
- [ ] Configurable Keybindings
|
||||
- [ ] Configurable Color Themes
|
||||
- [ ] Spawning Subprocesses (help needed - have a look at https://github.com/jesseduffield/lazygit/pull/18)
|
||||
- [ ] Maintainability
|
||||
- [ ] Performance
|
||||
- [ ] i18n
|
||||
### 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)
|
||||
[](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
|
||||
|
||||
### Debugging Locally
|
||||
Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to view the program and its log output side by side
|
||||
|
||||
## Donate
|
||||
If you would like to support the development of lazygit, please donate
|
||||
|
||||
[](https://donorbox.org/lazygit)
|
||||
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
|
||||
|
||||
## Work in progress
|
||||
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).
|
||||
## FAQ
|
||||
|
||||
### I'm struggling to see the selected line
|
||||
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
|
||||
|
||||
## Social
|
||||
|
||||
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:
|
||||
|
||||
- [GitUI](https://github.com/Extrawurst/gitui)
|
||||
- [tig](https://github.com/jonas/tig)
|
||||
|
||||
343
docs/Config.md
343
docs/Config.md
@@ -1,61 +1,219 @@
|
||||
# User Config:
|
||||
# User Config
|
||||
|
||||
## Default:
|
||||
Default path for the config file:
|
||||
|
||||
```
|
||||
* Linux: `~/.config/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
## Default
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
allBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scrool up
|
||||
scrollDownMain: '<pgdown>' # main panel scrool down
|
||||
scrollUpMain-alt1: 'K' # main panel scrool up
|
||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
```
|
||||
|
||||
## Platform Defaults:
|
||||
## Platform Defaults
|
||||
|
||||
### Windows:
|
||||
### Windows
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux:
|
||||
### Linux
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX:
|
||||
### OSX
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Recommended Config Values:
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -r {{filename}}'
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes:
|
||||
## Color Attributes
|
||||
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
The available attributes are:
|
||||
@@ -73,6 +231,157 @@ The available attributes are:
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
## Example Coloring:
|
||||
## Light terminal theme
|
||||
|
||||

|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## Struggling to see selected line
|
||||
|
||||
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
```
|
||||
|
||||
The following has also worked for a couple of people:
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
- blue
|
||||
```
|
||||
|
||||
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
|
||||
|
||||
If you're still having trouble please raise an issue.
|
||||
|
||||
## Example Coloring
|
||||
|
||||

|
||||
|
||||
## Keybindings
|
||||
|
||||
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
|
||||
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
|
||||
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
|
||||
the pull request. You can do so on your `config.yml` file using the following syntax:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
"<gitDomain>": "<provider>:<webDomain>"
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
|
||||
- `provider` is one of `github`, `bitbucket` or `gitlab`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: "[$1] "
|
||||
```
|
||||
|
||||
## Custom git log command
|
||||
|
||||
You can override the `git log` command that's used to render the log of the selected branch like so:
|
||||
|
||||
```
|
||||
git:
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium --oneline {{branchName}} --"
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||

|
||||
|
||||
## Launching not in a repository behaviour
|
||||
|
||||
By default, when launching lazygit from a directory that is not a repository,
|
||||
you will be prompted to choose if you would like to initialize a repo. You can
|
||||
override this behaviour in the config with one of the following:
|
||||
|
||||
```yaml
|
||||
# for default prompting behaviour
|
||||
notARepository: 'prompt'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to skip and initialize a new repo
|
||||
notARepository: 'create'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to skip without creating a new repo
|
||||
notARepository: 'skip'
|
||||
```
|
||||
|
||||
138
docs/Custom_Command_Keybindings.md
Normal file
138
docs/Custom_Command_Keybindings.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Custom Command Keybindings
|
||||
|
||||
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
|
||||
|
||||
```yml
|
||||
customCommands:
|
||||
- key: '<c-r>'
|
||||
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
|
||||
context: 'commits'
|
||||
- key: 'a'
|
||||
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name}}"
|
||||
context: 'files'
|
||||
description: 'toggle file staged'
|
||||
- key: 'C'
|
||||
command: "git commit"
|
||||
context: 'global'
|
||||
subprocess: true
|
||||
- key: 'n'
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'What kind of branch is it?'
|
||||
options:
|
||||
- name: 'feature'
|
||||
description: 'a feature branch'
|
||||
value: 'feature'
|
||||
- name: 'hotfix'
|
||||
description: 'a hotfix branch'
|
||||
value: 'hotfix'
|
||||
- name: 'release'
|
||||
description: 'a release branch'
|
||||
value: 'release'
|
||||
- type: 'input'
|
||||
title: 'What is the new branch name?'
|
||||
initialValue: ''
|
||||
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
|
||||
context: 'localBranches'
|
||||
loadingText: 'creating branch'
|
||||
```
|
||||
|
||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||
|
||||

|
||||
|
||||
Custom command keybindings will appear alongside inbuilt keybindings when you view the options menu by pressing 'x':
|
||||
|
||||

|
||||
|
||||
For a given custom command, here are the allowed fields:
|
||||
| _field_ | _description_ | required |
|
||||
|-----------------|----------------------|-|
|
||||
| key | the key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | yes |
|
||||
| command | the command to run | yes |
|
||||
| context | the context in which to listen for the key (see below) | yes |
|
||||
| subprocess | whether you want the command to run in a subprocess (necessary if you want to view the output of the command or provide user input) | no |
|
||||
| prompts | a list of prompts that will request user input before running the final command | no |
|
||||
| loadingText | text to display while waiting for command to finish | no |
|
||||
| description | text to display in the keybindings menu that appears when you press 'x' | no |
|
||||
|
||||
### Contexts
|
||||
|
||||
The permitted contexts are:
|
||||
|
||||
| _context_ | _description_ |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| status | the 'Status' tab |
|
||||
| files | the 'Files' tab |
|
||||
| localBranches | the 'Local Branches' tab |
|
||||
| remotes | the 'Remotes' tab |
|
||||
| remoteBranches | the context you get when pressing enter on a remote in the remotes tab |
|
||||
| tags | the 'Tags' tab |
|
||||
| commits | the 'Commits' tab |
|
||||
| reflogCommits | the 'Reflog' tab |
|
||||
| subCommits | the context you see when pressing enter on a branch |
|
||||
| commitFiles | the context you see when pressing enter on a commit or stash entry (warning, might be renamed in future) |
|
||||
| stash | the 'Stash' tab |
|
||||
| global | this keybinding will take affect everywhere |
|
||||
|
||||
### Prompts
|
||||
|
||||
The permitted prompt fields are:
|
||||
|
||||
| _field_ | _description_ | _required_ |
|
||||
| ------------ | -------------------------------------------------------------------------------- | ---------- |
|
||||
| type | one of 'input' or 'menu' | yes |
|
||||
| title | the title to display in the popup panel | no |
|
||||
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
|
||||
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
|
||||
|
||||
The permitted option fields are:
|
||||
| _field_ | _description_ | _required_ |
|
||||
|-----------------|----------------------|-|
|
||||
| name | the string which will appear first on the line | no |
|
||||
| description | the string which will appear second on the line | no |
|
||||
| value | the value that will be stored in `.PromptResponses` if the option is selected | yes |
|
||||
|
||||
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
|
||||
|
||||
```yml
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'What kind of branch is it?'
|
||||
options:
|
||||
- value: 'feature'
|
||||
- value: 'hotfix'
|
||||
- value: 'release'
|
||||
```
|
||||
|
||||
### Placeholder values
|
||||
|
||||
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/go/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
|
||||
|
||||
```
|
||||
SelectedLocalCommit
|
||||
SelectedReflogCommit
|
||||
SelectedSubCommit
|
||||
SelectedFile
|
||||
SelectedLocalBranch
|
||||
SelectedRemoteBranch
|
||||
SelectedRemote
|
||||
SelectedTag
|
||||
SelectedStashEntry
|
||||
SelectedCommitFile
|
||||
CheckedOutBranch
|
||||
```
|
||||
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
|
||||
### Keybinding collisions
|
||||
|
||||
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
|
||||
|
||||
### Debugging
|
||||
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
|
||||
|
||||
### More Examples
|
||||
|
||||
See the [wiki](https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium) page for more examples, and feel free to add your own custom commands to this page so others can benefit!
|
||||
64
docs/Custom_Pagers.md
Normal file
64
docs/Custom_Pagers.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Custom Pagers
|
||||
|
||||
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `o` in the Status panel).
|
||||
|
||||
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
|
||||
|
||||
## Default:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
```
|
||||
|
||||
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
|
||||
|
||||
## Delta:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: delta --dark --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Diff-so-fancy
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: diff-so-fancy
|
||||
```
|
||||
|
||||

|
||||
|
||||
## ydiff
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
|
||||
git:
|
||||
paging:
|
||||
colorArg: never
|
||||
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
|
||||
```
|
||||
|
||||

|
||||
|
||||
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
|
||||
|
||||
## Using git config
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: true
|
||||
```
|
||||
|
||||
If you set `useConfig: true`, lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
|
||||
88
docs/Integration_Tests.md
Normal file
88
docs/Integration_Tests.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# How To Make And Run Integration Tests For lazygit
|
||||
|
||||
Integration tests are located in `test/integration`. Each test will run a bash script to prepare a test repo, then replay a recorded lazygit session from within that repo, and then the resultant repo will be compared to an expected repo that was created upon the initial recording. Each integration test lives in its own directory, and the name of the directory becomes the name of the test. Within the directory must be the following files:
|
||||
|
||||
### `test.json`
|
||||
|
||||
An example of a `test.json` is:
|
||||
|
||||
```
|
||||
{ "description": "stage a file and commit the change", "speed": 20 }
|
||||
```
|
||||
|
||||
The `speed` key refers to the playback speed as a multiple of the original recording speed. So 20 means the test will run 20 times faster than the original recording speed. If a test fails for a given speed, it will drop the speed and re-test, until finally attempting the test at the original speed. If you omit the speed, it will default to 10.
|
||||
|
||||
### `setup.sh`
|
||||
|
||||
This is a bash script containing the instructions for creating the test repo from scratch. For example:
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
|
||||
cd $1
|
||||
|
||||
git init
|
||||
|
||||
git config user.email "CI@example.com"
|
||||
git config user.name "CI"
|
||||
|
||||
echo test1 > myfile1
|
||||
git add .
|
||||
git commit -am "myfile1"
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To run all tests
|
||||
```
|
||||
go test pkg/gui/gui_test.go
|
||||
```
|
||||
|
||||
To run them in parallel
|
||||
```
|
||||
PARALLEL=true go test pkg/gui/gui_test.go
|
||||
```
|
||||
|
||||
To run a single test
|
||||
```
|
||||
go test pkg/gui/gui_test.go -run /<test name>
|
||||
```
|
||||
|
||||
To run a test at a certain speed
|
||||
```
|
||||
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
|
||||
```
|
||||
|
||||
To update a snapshot
|
||||
```
|
||||
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
|
||||
```
|
||||
|
||||
## Creating a new test
|
||||
|
||||
To create a new test:
|
||||
1) Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test.
|
||||
2) Update the `setup.sh` any way you like
|
||||
3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
|
||||
4) From the lazygit root directory, run:
|
||||
```
|
||||
RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /<test name>
|
||||
```
|
||||
5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
|
||||
6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
|
||||
|
||||
The resulting directory will look like:
|
||||
```
|
||||
actual/ (the resulting repo after running the test, ignored by git)
|
||||
expected/ (the 'snapshot' repo)
|
||||
config/ (need not be present)
|
||||
test.json
|
||||
setup.sh
|
||||
recording.json
|
||||
```
|
||||
|
||||
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
|
||||
|
||||
## Feedback
|
||||
|
||||
If you think this process can be improved, let me know! It shouldn't be too hard to change things.
|
||||
@@ -1,85 +0,0 @@
|
||||
# Keybindings:
|
||||
|
||||
## Global:
|
||||
|
||||
<pre>
|
||||
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
|
||||
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
|
||||
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
|
||||
<kbd>q</kbd>: quit
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>shift</kbd>+<kbd>P</kbd>: push
|
||||
</pre>
|
||||
|
||||
## Status Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
</pre>
|
||||
|
||||
## Files Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
|
||||
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
|
||||
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
|
||||
<kbd>o</kbd>: open
|
||||
<kbd>e</kbd>: edit
|
||||
<kbd>s</kbd>: open in sublime (requires 'subl' command)
|
||||
<kbd>v</kbd>: open in vscode (requires 'code' command)
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
|
||||
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
|
||||
<kbd>shift</kbd>+<kbd>A</kbd>: abort merge
|
||||
</pre>
|
||||
|
||||
## Branches Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout branch
|
||||
<kbd>f</kbd>: force checkout branch
|
||||
<kbd>m</kbd>: merge into currently checked out branch
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>D</kbd>: force delete branch
|
||||
</pre>
|
||||
|
||||
## Commits Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash down (only available for topmost commit)
|
||||
<kbd>r</kbd>: rename commit
|
||||
<kbd>shift</kbd>+<kbd>R</kbd>: rename commit using git editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
</pre>
|
||||
|
||||
## Stash Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
</pre>
|
||||
|
||||
## Popup Panel:
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close/cancel
|
||||
<kbd>enter</kbd>: confirm
|
||||
<kbd>tab</kbd>: enter newline (if editing)
|
||||
</pre>
|
||||
|
||||
## Resolving Merge Conflicts (Diff Panel):
|
||||
|
||||
<pre>
|
||||
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
|
||||
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>z</kbd>: undo (only available while still inside diff panel)
|
||||
</pre>
|
||||
7
docs/README.md
Normal file
7
docs/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Documentation Overview
|
||||
|
||||
* [Configuration](./Config.md).
|
||||
* [Custom Commands](./Custom_Command_Keybindings.md)
|
||||
* [Custom Pagers](./Custom_Pagers.md)
|
||||
* [Keybindings](./keybindings)
|
||||
* [Undo/Redo](./Undoing.md)
|
||||
24
docs/Undoing.md
Normal file
24
docs/Undoing.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Undo/Redo in lazygit
|
||||
|
||||

|
||||
|
||||
## Keybindings:
|
||||
'z' to undo, 'ctrl+z' to redo
|
||||
|
||||
## How it works
|
||||
|
||||
If you're as clumsy as me you'll probably have felt the pain of botching an interactive rebase or doing a hard reset onto the wrong commit. Luckily, the reflog allows you to trace your steps and make things right again, but I personally can't stand trying to make sense of the reflog.
|
||||
|
||||
Lazygit can read through your reflog for you and walk back action by action so that you don't even need to read the reflog. If lazygit finds a reflog entry where you checked out a branch, we'll checkout the original branch. If the entry is from a commit being applied, we'll go back to the commit before that. If we hit an interactive rebase, we'll go back to the commit you were on just before you started it.
|
||||
|
||||
## You can even undo things you did outside of lazygit!
|
||||
|
||||
Because lazygit just uses the reflog to keep track of things, it doesn't matter whether you're trying to undo something you did in lazygit or directly on the command line. You can open lazygit for the first time and start undoing thing in your repo! Likewise, lazygit marks its undos/redos in the reflog so if you quit the application and come back, lazygit still knows where you're up to.
|
||||
|
||||
## Limitations
|
||||
|
||||
There are limitations: firstly, lazygit can only undo things that are recorded in the reflog. That means changes to your working tree or stash aren't covered. Secondly, anything permanent you do like pushing to a remote can't be undone. Thirdly, actions like creating a branch won't be undone, because they're not stored in the reflog.
|
||||
|
||||
If you are mid-rebase, undo/redo is not supported, because the reflog doesn't contain enough information about what specific things have happened inside that rebase. If you want to undo out of a rebase, it's best to abort the rebase (the default keybinding for bringing up rebase options is 'm').
|
||||
|
||||
Undo/Redo is a new feature so if you find a bug let us know. The worst case scenario is that you'll just need to look at your reflog and manually put yourself back on track.
|
||||
59
docs/keybindings/Custom_Keybindings.md
Normal file
59
docs/keybindings/Custom_Keybindings.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## Possible keybindings
|
||||
| Put in | You will get |
|
||||
|---------------|----------------|
|
||||
| `<f1>` | F1 |
|
||||
| `<f2>` | F2 |
|
||||
| `<f3>` | F3 |
|
||||
| `<f4>` | F4 |
|
||||
| `<f5>` | F5 |
|
||||
| `<f6>` | F6 |
|
||||
| `<f7>` | F7 |
|
||||
| `<f8>` | F8 |
|
||||
| `<f9>` | F9 |
|
||||
| `<f10>` | F10 |
|
||||
| `<f11>` | F11 |
|
||||
| `<f12>` | F12 |
|
||||
| `<insert>` | Insert |
|
||||
| `<delete>` | Delete |
|
||||
| `<home>` | Home |
|
||||
| `<end>` | End |
|
||||
| `<pgup>` | Pgup |
|
||||
| `<pgdown>` | Pgdn |
|
||||
| `<up>` | ArrowUp |
|
||||
| `<down>` | ArrowDown |
|
||||
| `<left>` | ArrowLeft |
|
||||
| `<right>` | ArrowRight |
|
||||
| `<tab>` | Tab |
|
||||
| `<enter>` | Enter |
|
||||
| `<esc>` | Esc |
|
||||
| `<backspace>` | Backspace |
|
||||
| `<c-space>` | CtrlSpace |
|
||||
| `<c-/>` | CtrlSlash |
|
||||
| `<space>` | Space |
|
||||
| `<c-a>` | CtrlA |
|
||||
| `<c-b>` | CtrlB |
|
||||
| `<c-c>` | CtrlC |
|
||||
| `<c-d>` | CtrlD |
|
||||
| `<c-e>` | CtrlE |
|
||||
| `<c-f>` | CtrlF |
|
||||
| `<c-g>` | CtrlG |
|
||||
| `<c-j>` | CtrlJ |
|
||||
| `<c-k>` | CtrlK |
|
||||
| `<c-l>` | CtrlL |
|
||||
| `<c-n>` | CtrlN |
|
||||
| `<c-o>` | CtrlO |
|
||||
| `<c-p>` | CtrlP |
|
||||
| `<c-q>` | CtrlQ |
|
||||
| `<c-r>` | CtrlR |
|
||||
| `<c-s>` | CtrlS |
|
||||
| `<c-t>` | CtrlT |
|
||||
| `<c-u>` | CtrlU |
|
||||
| `<c-v>` | CtrlV |
|
||||
| `<c-w>` | CtrlW |
|
||||
| `<c-x>` | CtrlX |
|
||||
| `<c-y>` | CtrlY |
|
||||
| `<c-z>` | CtrlZ |
|
||||
| `<c-4>` | Ctrl4 |
|
||||
| `<c-5>` | Ctrl5 |
|
||||
| `<c-6>` | Ctrl6 |
|
||||
| `<c-8>` | Ctrl8 |
|
||||
275
docs/keybindings/Keybindings_en.md
Normal file
275
docs/keybindings/Keybindings_en.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Lazygit Keybindings
|
||||
|
||||
## Global Keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<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 checked-out branch onto this branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: Return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Sub-commits)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Commit Files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Panel (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>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+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>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Files Panel (Files)
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<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 changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: view reset and remove submodule options
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Main Panel (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>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
</pre>
|
||||
|
||||
## Main Panel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
</pre>
|
||||
|
||||
## Stash Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: new branch
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
</pre>
|
||||
275
docs/keybindings/Keybindings_nl.md
Normal file
275
docs/keybindings/Keybindings_nl.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Globaale Sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
|
||||
<kbd>_</kbd>: vorige schermmode
|
||||
<kbd>:</kbd>: voor aangepast commando uit
|
||||
<kbd>|</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak een pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>i</kbd>: laat git-flow opties zien
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: voeg een nieuwe remote toe
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Sub-commits)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: verwijder tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
|
||||
<kbd>c</kbd>: bestand uitchecken
|
||||
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: toggle bestand inbegrepen in patch
|
||||
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: hernoem commit met editor
|
||||
<kbd>g</kbd>: reset naar deze commit
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>F</kbd>: creëer fixup commit voor deze commit
|
||||
<kbd>S</kbd>: squash bovenstaande commits
|
||||
<kbd>d</kbd>: verwijder commit
|
||||
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
|
||||
<kbd>e</kbd>: wijzig commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
|
||||
<kbd>t</kbd>: commit ongedaan maken
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Bestanden)
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: voeg toe aan .gitignore
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: view reset and remove submodule options
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Merggen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
<kbd>▼</kbd>: selecteer onderste hunk
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Patch Bouwen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
</pre>
|
||||
|
||||
## Menu Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit menu
|
||||
</pre>
|
||||
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
</pre>
|
||||
|
||||
## Status Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle takken van het houtblok laten zien
|
||||
</pre>
|
||||
275
docs/keybindings/Keybindings_pl.md
Normal file
275
docs/keybindings/Keybindings_pl.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Lazygit Keybindings
|
||||
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Sub-commits)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>enter</kbd>: view commits
|
||||
</pre>
|
||||
|
||||
## Commit files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commity)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: ściśnij w dół
|
||||
<kbd>r</kbd>: przemianuj commit
|
||||
<kbd>R</kbd>: przemianuj commit w edytorze
|
||||
<kbd>g</kbd>: zresetuj do tego commita
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+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>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Pliki)
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: commituj zmiany
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
|
||||
<kbd>C</kbd>: commituj zmiany używając edytora z gita
|
||||
<kbd>space</kbd>: przełącz zatwierdzenie
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: dodaj do .gitignore
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj pliki
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: view reset and remove submodule options
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
</pre>
|
||||
|
||||
## Main Panel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<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>: cofnij
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
</pre>
|
||||
|
||||
## Main Panel (Zatwierdzanie)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>c</kbd>: commituj zmiany
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commituj zmiany używając edytora z gita
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
</pre>
|
||||
|
||||
## Schowek Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj plik konfiguracyjny
|
||||
<kbd>o</kbd>: otwórz plik konfiguracyjny
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokazywać wszystkie logi branżowe
|
||||
</pre>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
91
go.mod
91
go.mod
@@ -1,62 +1,47 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.15.21
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cli/safeexec v1.0.0
|
||||
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/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-ini/ini v1.38.2
|
||||
github.com/gdamore/tcell/v2 v2.2.0 // indirect
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
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-20180921065632-03e26ff3f1de
|
||||
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/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
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/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/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
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
332
go.sum
332
go.sum
@@ -1,119 +1,251 @@
|
||||
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/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8=
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
|
||||
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
|
||||
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886 h1:NAFoy+QgUpERgK3y1xiVh5HcOvSeZHpXTTo5qnvnuK4=
|
||||
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
|
||||
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
|
||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
|
||||
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||
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-20180919095827-4fca348422d8 h1:XxX+IqNOFDh1PnU4eZDzUomoKbuKCvwyEm5an/IxLQU=
|
||||
github.com/jesseduffield/gocui v0.0.0-20180919095827-4fca348422d8/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
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/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
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.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
|
||||
github.com/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/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/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/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/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=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
123
main.go
123
main.go
@@ -1,15 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -17,38 +21,119 @@ var (
|
||||
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 {
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := ""
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
dump := ""
|
||||
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
|
||||
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
|
||||
|
||||
versionFlag := false
|
||||
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
|
||||
|
||||
debuggingFlag := false
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
||||
|
||||
logFlag := false
|
||||
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
configFlag := false
|
||||
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
|
||||
|
||||
configDirFlag := false
|
||||
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
|
||||
|
||||
useConfigDir := ""
|
||||
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
||||
|
||||
workTree := ""
|
||||
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
||||
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if repoPath != "" {
|
||||
if workTree != "" || gitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
workTree = repoPath
|
||||
gitDir = filepath.Join(repoPath, ".git")
|
||||
}
|
||||
|
||||
if useConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
env.SetGitWorkTreeEnv(workTree)
|
||||
}
|
||||
|
||||
if gitDir != "" {
|
||||
env.SetGitDirEnv(gitDir)
|
||||
}
|
||||
|
||||
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())
|
||||
if configFlag {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n", buf.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configDirFlag {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if logFlag {
|
||||
app.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
if err := os.Chdir(workTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app, err := app.Setup(appConfig)
|
||||
if err != nil {
|
||||
app.Log.Error(err.Error())
|
||||
log.Fatal(err.Error())
|
||||
app, err := app.NewApp(appConfig, filterPath)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run()
|
||||
}
|
||||
|
||||
app.Gui.RunWithSubprocesses()
|
||||
if err != nil {
|
||||
if errorMessage, known := app.KnownError(err); known {
|
||||
log.Fatal(errorMessage)
|
||||
}
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
|
||||
}
|
||||
}
|
||||
|
||||
299
pkg/app/app.go
299
pkg/app/app.go
@@ -1,15 +1,26 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/heroku/rollrus"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -18,45 +29,64 @@ import (
|
||||
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
|
||||
Config config.AppConfigurer
|
||||
Log *logrus.Entry
|
||||
OSCommand *oscommands.OSCommand
|
||||
GitCommand *commands.GitCommand
|
||||
Gui *gui.Gui
|
||||
Tr *i18n.TranslationSet
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
ClientContext string
|
||||
}
|
||||
|
||||
type errorMapping struct {
|
||||
originalError string
|
||||
newError string
|
||||
}
|
||||
|
||||
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
|
||||
func newDevelopmentLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
func getLogLevel() logrus.Level {
|
||||
strLevel := os.Getenv("LOG_LEVEL")
|
||||
level, err := logrus.ParseLevel(strLevel)
|
||||
if err != nil {
|
||||
return logrus.DebugLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(getLogLevel())
|
||||
logPath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
file, err := os.OpenFile(logPath, 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
|
||||
logger.SetOutput(file)
|
||||
return logger
|
||||
}
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
environment := "production"
|
||||
if config.GetDebug() {
|
||||
environment = "development"
|
||||
log = newDevelopmentLogger()
|
||||
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
|
||||
log = newDevelopmentLogger(config)
|
||||
} else {
|
||||
log = newProductionLogger(config)
|
||||
}
|
||||
if config.GetUserConfig().GetString("reporting") == "on" {
|
||||
// this isn't really a secret token: it only has permission to push new rollbar items
|
||||
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)
|
||||
log.Hooks.Add(hook)
|
||||
}
|
||||
|
||||
// highly recommended: tail -f development.log | humanlog
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
return log.WithFields(logrus.Fields{
|
||||
"debug": config.GetDebug(),
|
||||
"version": config.GetVersion(),
|
||||
@@ -65,33 +95,183 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
})
|
||||
}
|
||||
|
||||
// Setup bootstrap a new application
|
||||
func Setup(config config.AppConfigurer) (*App, error) {
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.OSCommand = commands.NewOSCommand(app.Log, config)
|
||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
||||
|
||||
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 = oscommands.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)
|
||||
|
||||
showRecentRepos, err := app.setupRepo()
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
|
||||
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *App) validateGitVersion() error {
|
||||
output, err := app.OSCommand.RunCommandWithOutput("git --version")
|
||||
// if we get an error anywhere here we'll show the same status
|
||||
minVersionError := errors.New(app.Tr.MinGitVersionError)
|
||||
if err != nil {
|
||||
return minVersionError
|
||||
}
|
||||
|
||||
if isGitVersionValid(output) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return minVersionError
|
||||
}
|
||||
|
||||
func isGitVersionValid(versionStr string) bool {
|
||||
// output should be something like: 'git version 2.23.0 (blah)'
|
||||
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
|
||||
matches := re.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
gitVersion := matches[1]
|
||||
majorVersion, err := strconv.Atoi(gitVersion[0:1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if majorVersion < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (app *App) setupRepo() (bool, error) {
|
||||
if err := app.validateGitVersion(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if env.GetGitDirEnv() != "" {
|
||||
// we've been given the git dir directly. We'll verify this dir when initializing our GitCommand object
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// if we are not in a git repo, we ask if we want to `git init`
|
||||
if err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
info, _ := os.Stat(filepath.Join(cwd, ".git"))
|
||||
if info != nil && info.IsDir() {
|
||||
return false, err // Current directory appears to be a git repository.
|
||||
}
|
||||
|
||||
shouldInitRepo := true
|
||||
notARepository := app.Config.GetUserConfig().NotARepository
|
||||
if notARepository == "prompt" {
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.CreateRepo)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
} else if notARepository == "skip" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
|
||||
if !shouldInitRepo {
|
||||
// check if we have a recent repo we can open
|
||||
recentRepos := app.Config.GetAppState().RecentRepos
|
||||
if len(recentRepos) > 0 {
|
||||
var err error
|
||||
// try opening each repo in turn, in case any have been deleted
|
||||
for _, repoDir := range recentRepos {
|
||||
if err = os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := app.OSCommand.RunCommand("git init"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
if app.ClientContext == "INTERACTIVE_REBASE" {
|
||||
return app.Rebase()
|
||||
}
|
||||
|
||||
if app.ClientContext == "EXIT_IMMEDIATELY" {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := app.Gui.RunAndHandleError()
|
||||
return err
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// 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], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
|
||||
// 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 {
|
||||
@@ -102,3 +282,66 @@ func (app *App) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
|
||||
func (app *App) KnownError(err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
knownErrorMessages := []string{app.Tr.MinGitVersionError}
|
||||
|
||||
for _, message := range knownErrorMessages {
|
||||
if errorMessage == message {
|
||||
return message, true
|
||||
}
|
||||
}
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "fatal: not a git repository",
|
||||
newError: app.Tr.NotARepository,
|
||||
},
|
||||
}
|
||||
|
||||
for _, mapping := range mappings {
|
||||
if strings.Contains(errorMessage, mapping.originalError) {
|
||||
return mapping.newError, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func TailLogs() {
|
||||
logFilePath, err := config.LogPath()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
||||
|
||||
_, err = os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatal("Log file does not exist. Run `lazygit --debug` first to create the log file")
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := humanlog.DefaultOptions
|
||||
opts.Truncates = false
|
||||
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
44
pkg/app/app_test.go
Normal file
44
pkg/app/app_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsGitVersionValid(t *testing.T) {
|
||||
type scenario struct {
|
||||
versionStr string
|
||||
expectedResult bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"git version 1.9.0",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"git version 1.9.0 (Apple Git-128)",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"git version 2.4.0",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"git version 2.24.3 (Apple Git-128)",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.versionStr, func(t *testing.T) {
|
||||
result := isGitVersionValid(s.versionStr)
|
||||
assert.Equal(t, result, s.expectedResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Branch : A git branch
|
||||
// duplicating this for now
|
||||
type Branch struct {
|
||||
Name string
|
||||
Recency string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
func (b *Branch) GetDisplayStrings() []string {
|
||||
return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
|
||||
}
|
||||
|
||||
// GetColor branch color
|
||||
func (b *Branch) GetColor() color.Attribute {
|
||||
switch b.getType() {
|
||||
case "feature":
|
||||
return color.FgGreen
|
||||
case "bugfix":
|
||||
return color.FgYellow
|
||||
case "hotfix":
|
||||
return color.FgRed
|
||||
default:
|
||||
return color.FgWhite
|
||||
}
|
||||
}
|
||||
|
||||
// expected to return feature/bugfix/hotfix or blank string
|
||||
func (b *Branch) getType() string {
|
||||
return strings.Split(b.Name, "/")[0]
|
||||
}
|
||||
157
pkg/commands/branches.go
Normal file
157
pkg/commands/branches.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.RunCommand("git checkout -b %s %s", name, base)
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
// the first returned string is the name and the second is the displayname
|
||||
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
||||
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
}
|
||||
output, err := c.RunCommandWithOutput("git branch --contains")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, line := range utils.SplitLines(output) {
|
||||
re := regexp.MustCompile(CurrentBranchNameRegex)
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) > 0 {
|
||||
branchName = match[1]
|
||||
displayBranchName := match[0][2:]
|
||||
return branchName, displayBranchName, nil
|
||||
}
|
||||
}
|
||||
return "HEAD", "HEAD", 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("%s %s", command, branch)
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
type CheckoutOptions struct {
|
||||
Force bool
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if options.Force {
|
||||
forceArg = "--force "
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
// 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) {
|
||||
cmdStr := c.GetBranchGraphCmdStr(branchName)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
|
||||
return strings.TrimSpace(output), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
|
||||
templateValues := map[string]string{
|
||||
"branchName": branchName,
|
||||
}
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
return c.GetCommitDifferences("HEAD", "HEAD@{u}")
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
|
||||
return c.GetCommitDifferences(branchName, branchName+"@{u}")
|
||||
}
|
||||
|
||||
// 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(command, to, from)
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||
}
|
||||
|
||||
type MergeOpts struct {
|
||||
FastForwardOnly bool
|
||||
}
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.RunCommand("git merge --abort")
|
||||
}
|
||||
|
||||
func (c *GitCommand) IsHeadDetached() bool {
|
||||
err := c.RunCommand("git symbolic-ref -q HEAD")
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Pushed bool
|
||||
Merged bool
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
func (c *Commit) GetDisplayStrings() []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgGreen)
|
||||
green := color.New(color.FgYellow)
|
||||
white := color.New(color.FgWhite)
|
||||
|
||||
shaColor := yellow
|
||||
if c.Pushed {
|
||||
shaColor = red
|
||||
} else if !c.Merged {
|
||||
shaColor = green
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
|
||||
}
|
||||
98
pkg/commands/commits.go
Normal file
98
pkg/commands/commits.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
|
||||
cmdStr := "git log -1 --pretty=%s"
|
||||
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
|
||||
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
|
||||
}
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (c *GitCommand) Revert(sha string) error {
|
||||
return c.RunCommand("git revert %s", sha)
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (c *GitCommand) CherryPickCommits(commits []*models.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)
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
return c.RunCommand("git commit --fixup=%s", sha)
|
||||
}
|
||||
48
pkg/commands/config.go
Normal file
48
pkg/commands/config.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("GIT_PAGER") != "" {
|
||||
return os.Getenv("GIT_PAGER")
|
||||
}
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
return strings.Split(trimmedOutput, "\n")[0]
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPager(width int) string {
|
||||
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
|
||||
if useConfig {
|
||||
pager := c.ConfiguredPager()
|
||||
return strings.Split(pager, "| less")[0]
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"columnWidth": strconv.Itoa(width/2 - 6),
|
||||
}
|
||||
|
||||
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
|
||||
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) colorArg() string {
|
||||
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
25
pkg/commands/dummies.go
Normal file
25
pkg/commands/dummies.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyGitCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommand() *GitCommand {
|
||||
return NewDummyGitCommandWithOSCommand(oscommands.NewDummyOSCommand())
|
||||
}
|
||||
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
removeFile: func(string) error { return nil },
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package commands
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
// File : A file from git status
|
||||
// duplicating this for now
|
||||
type File struct {
|
||||
Name string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
DisplayString string
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
func (f *File) GetDisplayStrings() []string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
|
||||
output := green.Sprint(f.DisplayString[0:1])
|
||||
output += red.Sprint(f.DisplayString[1:3])
|
||||
if f.HasUnstagedChanges {
|
||||
output += red.Sprint(f.Name)
|
||||
} else {
|
||||
output += green.Sprint(f.Name)
|
||||
}
|
||||
return []string{output}
|
||||
}
|
||||
343
pkg/commands/files.go
Normal file
343
pkg/commands/files.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
// UnstageAll unstages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.RunCommand("git reset")
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
// we accept an array of filenames for the cases where a file has been renamed i.e.
|
||||
// we accept the current name and the previous name
|
||||
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
|
||||
command := "git rm --cached --force -- %s"
|
||||
if reset {
|
||||
command = "git reset HEAD -- %s"
|
||||
}
|
||||
|
||||
for _, name := range fileNames {
|
||||
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
|
||||
|
||||
if !file.IsRename() {
|
||||
return nil, nil, errors.New("Expected renamed file")
|
||||
}
|
||||
|
||||
// we've got a file that represents a rename from one file to another. Here we will refetch
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file.
|
||||
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
var beforeFile *models.File
|
||||
var afterFile *models.File
|
||||
for _, f := range filesWithoutRenames {
|
||||
if f.Name == file.PreviousName {
|
||||
beforeFile = f
|
||||
}
|
||||
|
||||
if f.Name == file.Name {
|
||||
afterFile = f
|
||||
}
|
||||
}
|
||||
|
||||
if beforeFile == nil || afterFile == nil {
|
||||
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
|
||||
}
|
||||
|
||||
if beforeFile.IsRename() || afterFile.IsRename() {
|
||||
// probably won't happen but we want to ensure we don't get an infinite loop
|
||||
return nil, nil, errors.New("Nested rename found")
|
||||
}
|
||||
|
||||
return beforeFile, afterFile, nil
|
||||
}
|
||||
|
||||
// DiscardAllFileChanges directly
|
||||
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
if file.IsRename() {
|
||||
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(afterFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
|
||||
if file.ShortStatus == "AA" {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return c.RunCommand("git rm %s", quotedFileName)
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.Added {
|
||||
return c.removeFile(file.Name)
|
||||
}
|
||||
return c.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
|
||||
// this could be more efficient but we would need to handle all the edge cases
|
||||
return node.ForEachFile(c.DiscardAllFileChanges)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
|
||||
if err := c.RemoveUntrackedDirFiles(node); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
|
||||
untrackedFilePaths := node.GetPathsMatching(
|
||||
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
|
||||
)
|
||||
|
||||
for _, path := range untrackedFilePaths {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
return c.RunCommand("git checkout -- %s", quotedFileName)
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
func (c *GitCommand) Ignore(filename string) error {
|
||||
return c.OSCommand.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
|
||||
c.Log.Infof("saving temporary patch to %s", filepath)
|
||||
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flagStr := ""
|
||||
for _, flag := range flags {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
|
||||
colorArg := c.colorArg()
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
||||
return c.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.StageFile(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.RunCommand("git checkout -- .")
|
||||
}
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.RunCommand("git rm -r --cached %s", name)
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.RunCommand("git clean -fd")
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
func (c *GitCommand) ResetAndClean() error {
|
||||
submoduleConfigs, err := c.GetSubmoduleConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(submoduleConfigs) > 0 {
|
||||
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.ResetHard("HEAD"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, GIT_EDITOR, VISUAL, EDITOR, then vi
|
||||
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.OSCommand.RunCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
|
||||
|
||||
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
||||
}
|
||||
@@ -1,574 +1,229 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"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")
|
||||
// this takes something like:
|
||||
// * (HEAD detached at 264fc6f5)
|
||||
// remotes
|
||||
// and returns '264fc6f5' as the second match
|
||||
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *oscommands.OSCommand
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGitConfigValue func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
|
||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||
PushToCurrent bool
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
|
||||
var repo *gogit.Repository
|
||||
|
||||
// see what our default push behaviour is
|
||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
||||
pushToCurrent := false
|
||||
if err != nil {
|
||||
log.Errorf("error reading git config: %v", err)
|
||||
} else {
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
|
||||
return gitCommand, nil
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
for {
|
||||
f, err := stat(".git")
|
||||
gitDir := env.GetGitDirEnv()
|
||||
if gitDir != "" {
|
||||
// we've been given the git directory explicitly so no need to navigate to it
|
||||
_, err := stat(gitDir)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
if err == nil && f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory)
|
||||
|
||||
for {
|
||||
_, err := stat(".git")
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
if err = chdir(".."); err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atRoot := currentPath == filepath.Dir(currentPath)
|
||||
if atRoot {
|
||||
// we should never really land here: the code that creates GitCommand should
|
||||
// verify we're in a git directory
|
||||
return errors.New("Must open lazygit in a git repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
|
||||
repository, err = openGitRepository(".")
|
||||
|
||||
// resolvePath takes a path containing a symlink and returns the true path
|
||||
func resolvePath(path string) (string, error) {
|
||||
l, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
|
||||
return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
|
||||
}
|
||||
|
||||
return
|
||||
return "", err
|
||||
}
|
||||
|
||||
worktree, err = repository.Worktree()
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
if l.Mode()&os.ModeSymlink == 0 {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
return
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer) (*GitCommand, error) {
|
||||
var worktree *gogit.Worktree
|
||||
var repo *gogit.Repository
|
||||
|
||||
fs := []func() error{
|
||||
func() error {
|
||||
return verifyInGitRepo(osCommand.RunCommand)
|
||||
},
|
||||
func() error {
|
||||
return navigateToRepoRootDirectory(os.Stat, os.Chdir)
|
||||
},
|
||||
func() error {
|
||||
var err error
|
||||
repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if err := f(); err != nil {
|
||||
func setupRepository(openGitRepository func(string) (*gogit.Repository, error), gitConfigParseErrorStr string) (*gogit.Repository, error) {
|
||||
unresolvedPath := env.GetGitDirEnv()
|
||||
if unresolvedPath == "" {
|
||||
var err error
|
||||
unresolvedPath, err = os.Getwd()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Worktree: worktree,
|
||||
Repo: repo,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entryies
|
||||
func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
|
||||
stashEntries := []*StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) *StashEntry {
|
||||
return &StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
DisplayString: line,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
|
||||
}
|
||||
|
||||
// GetStatusFiles git status files
|
||||
func (c *GitCommand) GetStatusFiles() []*File {
|
||||
statusOutput, _ := c.GitStatus()
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
|
||||
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
|
||||
|
||||
file := &File{
|
||||
Name: filename,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
HasMergeConflicts: change == "UU",
|
||||
Type: c.OSCommand.FileType(filename),
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
c.Log.Info(files) // TODO: use a dumper-esque log here
|
||||
return files
|
||||
}
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetBranchName branch name
|
||||
func (c *GitCommand) GetBranchName() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
}
|
||||
|
||||
// ResetHard does the equivalent of `git reset --hard HEAD`
|
||||
func (c *GitCommand) ResetHard() error {
|
||||
return c.Worktree.Reset(&gogit.ResetOptions{Mode: gogit.HardReset})
|
||||
}
|
||||
|
||||
// UpstreamDifferenceCount checks how many pushables/pullables there are for the
|
||||
// current branch
|
||||
func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
|
||||
pushableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := c.OSCommand.RunCommandWithOutput("git rev-list HEAD..@{u} --count")
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
|
||||
}
|
||||
|
||||
// GetCommitsToPush Returns the sha's of the commits that have not yet been pushed
|
||||
// to the remote branch of the current branch, a map is returned to ease look up
|
||||
func (c *GitCommand) GetCommitsToPush() map[string]bool {
|
||||
pushables := map[string]bool{}
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
|
||||
if err != nil {
|
||||
return pushables
|
||||
}
|
||||
for _, p := range utils.SplitLines(o) {
|
||||
pushables[p] = true
|
||||
}
|
||||
|
||||
return pushables
|
||||
}
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
|
||||
}
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch() error {
|
||||
return c.OSCommand.RunCommand("git fetch")
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
|
||||
}
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
|
||||
}
|
||||
|
||||
func (c *GitCommand) CurrentBranchName() (string, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return utils.TrimTrailingNewline(output), nil
|
||||
}
|
||||
|
||||
// DeleteBranch delete branch
|
||||
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
||||
command := "git branch -d"
|
||||
|
||||
if force {
|
||||
command = "git branch -D"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
|
||||
}
|
||||
|
||||
// ListStash list stash
|
||||
func (c *GitCommand) ListStash() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash list")
|
||||
}
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.OSCommand.RunCommand("git merge --abort")
|
||||
}
|
||||
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
||||
}
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, amend bool) (*exec.Cmd, error) {
|
||||
amendParam := ""
|
||||
if amend {
|
||||
amendParam = " --amend"
|
||||
}
|
||||
command := fmt.Sprintf("git commit%s -m %s", amendParam, c.OSCommand.Quote(message))
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Pull pulls from repo
|
||||
func (c *GitCommand) Pull() error {
|
||||
return c.OSCommand.RunCommand("git pull --no-edit")
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease "
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName))
|
||||
}
|
||||
|
||||
// SquashPreviousTwoCommits squashes a commit down to the one below it
|
||||
// retaining the message of the higher commit
|
||||
func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
|
||||
// TODO: test this
|
||||
if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: if password is required, we need to return a subprocess
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", c.OSCommand.Quote(message)))
|
||||
}
|
||||
|
||||
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
|
||||
// retaining the commit message of the lower commit
|
||||
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("git checkout -q %s", shaValue),
|
||||
fmt.Sprintf("git reset --soft %s^", shaValue),
|
||||
fmt.Sprintf("git commit --amend -C %s^", shaValue),
|
||||
fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
|
||||
}
|
||||
for _, command := range commands {
|
||||
c.Log.Info(command)
|
||||
|
||||
if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil {
|
||||
ret := output
|
||||
// We are already in an error state here so we're just going to append
|
||||
// the output of these commands
|
||||
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue))
|
||||
ret += output
|
||||
output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName))
|
||||
ret += output
|
||||
|
||||
c.Log.Info(ret)
|
||||
return errors.New(ret)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.OSCommand.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
// UnstageAll stages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.OSCommand.RunCommand("git reset")
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
command := "git rm --cached %s"
|
||||
if tracked {
|
||||
command = "git reset HEAD %s"
|
||||
}
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(fileName)))
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
func (c *GitCommand) GitStatus() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --short")
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||
}
|
||||
|
||||
// RemoveFile directly
|
||||
func (c *GitCommand) RemoveFile(file *File) error {
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", file.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !file.Tracked {
|
||||
return c.removeFile(file.Name)
|
||||
}
|
||||
// if the file is tracked, we assume you want to just check it out
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", file.Name))
|
||||
}
|
||||
|
||||
// Checkout checks out a branch, with --force if you set the force arg to true
|
||||
func (c *GitCommand) Checkout(branch string, force bool) error {
|
||||
forceArg := ""
|
||||
if force {
|
||||
forceArg = "--force "
|
||||
}
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch))
|
||||
}
|
||||
|
||||
// AddPatch prepares a subprocess for adding a patch by patch
|
||||
// this will eventually be swapped out for a better solution inside the Gui
|
||||
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
|
||||
}
|
||||
|
||||
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||
func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||
}
|
||||
|
||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
// Currently it limits the result to 100 commits, but when we get async stuff
|
||||
// working we can do lazy loading
|
||||
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) getMergeBase() (string, error) {
|
||||
currentBranch, err := c.CurrentBranchName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseBranch := "master"
|
||||
if strings.HasPrefix(currentBranch, "feature/") {
|
||||
baseBranch = "develop"
|
||||
}
|
||||
|
||||
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
|
||||
if err != nil {
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
c.Log.Error(err)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *GitCommand) GetCommits() ([]*Commit, error) {
|
||||
pushables := c.GetCommitsToPush()
|
||||
log := c.GetLog()
|
||||
|
||||
lines := utils.SplitLines(log)
|
||||
commits := make([]*Commit, len(lines))
|
||||
// now we can split it up and turn it into commits
|
||||
for i, line := range lines {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
_, pushed := pushables[sha]
|
||||
commits[i] = &Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Pushed: pushed,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
}
|
||||
}
|
||||
return c.setCommitMergedStatuses(commits)
|
||||
}
|
||||
|
||||
func (c *GitCommand) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
ancestor, err := c.getMergeBase()
|
||||
path, err := resolvePath(unresolvedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ancestor == "" {
|
||||
return commits, nil
|
||||
}
|
||||
passedAncestor := false
|
||||
for i, commit := range commits {
|
||||
if strings.HasPrefix(ancestor, commit.Sha) {
|
||||
passedAncestor = true
|
||||
}
|
||||
commits[i].Merged = passedAncestor
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// GetLog gets the git log (currently limited to 30 commits for performance
|
||||
// until we work out lazy loading
|
||||
func (c *GitCommand) GetLog() string {
|
||||
// currently limiting to 30 for performance reasons
|
||||
// TODO: add lazyloading when you scroll down
|
||||
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
|
||||
repository, err := openGitRepository(path)
|
||||
|
||||
if err != nil {
|
||||
// assume if there is an error there are no commits yet for this branch
|
||||
return ""
|
||||
if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
|
||||
return nil, errors.New(gitConfigParseErrorStr)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result
|
||||
return repository, err
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
func (c *GitCommand) Ignore(filename string) error {
|
||||
return c.OSCommand.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
// Show shows the diff of a commit
|
||||
func (c *GitCommand) Show(sha string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
fileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||
cachedArg = "--cached"
|
||||
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
|
||||
if env.GetGitDirEnv() != "" {
|
||||
return env.GetGitDirEnv(), nil
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
}
|
||||
command := fmt.Sprintf("git diff --color %s %s %s", cachedArg, trackedArg, fileName)
|
||||
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(command)
|
||||
return s
|
||||
f, err := stat(".git")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return ".git", nil
|
||||
}
|
||||
|
||||
fileBytes, err := readFile(".git")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileContent := string(fileBytes)
|
||||
if !strings.HasPrefix(fileContent, "gitdir: ") {
|
||||
return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
||||
return osCommand.RunCommand("git rev-parse --git-dir")
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
// TODO: have this retry logic in other places we run the command
|
||||
waitTime := 50 * time.Millisecond
|
||||
retryCount := 5
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
|
||||
if err != nil {
|
||||
// if we have an error based on the index lock, we should wait a bit and then retry
|
||||
if strings.Contains(output, ".git/index.lock") {
|
||||
c.Log.Error(output)
|
||||
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
||||
attempt++
|
||||
time.Sleep(waitTime)
|
||||
if attempt < retryCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
56
pkg/commands/gitconfig.go
Normal file
56
pkg/commands/gitconfig.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
)
|
||||
|
||||
// including license from https://github.com/tcnksm/go-gitconfig because this file is an adaptation of that repo's code
|
||||
// Copyright (c) 2014 tcnksm
|
||||
|
||||
// MIT License
|
||||
|
||||
// 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.
|
||||
|
||||
func getGitConfigValue(key string) (string, error) {
|
||||
gitArgs := []string{"config", "--get", "--null", key}
|
||||
var stdout bytes.Buffer
|
||||
cmd := secureexec.Command("git", gitArgs...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = ioutil.Discard
|
||||
|
||||
err := cmd.Run()
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
if waitStatus.ExitStatus() == 1 {
|
||||
return "", fmt.Errorf("the key `%s` is not found", key)
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimRight(stdout.String(), "\000"), nil
|
||||
}
|
||||
161
pkg/commands/loading_branches.go
Normal file
161
pkg/commands/loading_branches.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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 *GitCommand
|
||||
ReflogCommits []*models.Commit
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
ReflogCommits: reflogCommits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
|
||||
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
|
||||
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
branches := make([]*models.Branch, 0, len(outputLines))
|
||||
for _, line := range outputLines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
branches = append(branches, branch)
|
||||
continue
|
||||
}
|
||||
|
||||
branch.UpstreamName = upstreamName
|
||||
|
||||
track := split[3]
|
||||
re := regexp.MustCompile(`ahead (\d+)`)
|
||||
match := re.FindStringSubmatch(track)
|
||||
if len(match) > 1 {
|
||||
branch.Pushables = match[1]
|
||||
} else {
|
||||
branch.Pushables = "0"
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(`behind (\d+)`)
|
||||
match = re.FindStringSubmatch(track)
|
||||
if len(match) > 1 {
|
||||
branch.Pullables = match[1]
|
||||
} else {
|
||||
branch.Pullables = "0"
|
||||
}
|
||||
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*models.Branch {
|
||||
branches := b.obtainBranches()
|
||||
|
||||
reflogBranches := b.obtainReflogBranches()
|
||||
|
||||
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
||||
branchesWithRecency := make([]*models.Branch, 0)
|
||||
outer:
|
||||
for _, reflogBranch := range reflogBranches {
|
||||
for j, branch := range branches {
|
||||
if branch.Head {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
||||
branch.Recency = reflogBranch.Recency
|
||||
branchesWithRecency = append(branchesWithRecency, branch)
|
||||
branches = append(branches[0:j], branches[j+1:]...)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = append(branchesWithRecency, branches...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = append(branches[0:i], branches[i+1:]...)
|
||||
branches = append([]*models.Branch{branch}, branches...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
||||
// int form against the branch to recalculate the time ago
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
|
||||
foundBranchesMap := map[string]bool{}
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
|
||||
for _, commit := range b.ReflogCommits {
|
||||
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranchesMap[branchName] {
|
||||
foundBranchesMap[branchName] = true
|
||||
reflogBranches = append(reflogBranches, &models.Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflogBranches
|
||||
}
|
||||
42
pkg/commands/loading_commit_files.go
Normal file
42
pkg/commands/loading_commit_files.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
}
|
||||
381
pkg/commands/loading_commits.go
Normal file
381
pkg/commands/loading_commits.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"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
|
||||
|
||||
const SEPARATION_CHAR = "|"
|
||||
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
OSCommand *oscommands.OSCommand
|
||||
Tr *i18n.TranslationSet
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
}
|
||||
}
|
||||
|
||||
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
|
||||
// then puts them into a commit object
|
||||
// example input:
|
||||
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
||||
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
sha := split[0]
|
||||
unixTimestamp := split[1]
|
||||
author := split[2]
|
||||
extraInfo := strings.TrimSpace(split[3])
|
||||
parentHashes := split[4]
|
||||
|
||||
message := strings.Join(split[5:], SEPARATION_CHAR)
|
||||
tags := []string{}
|
||||
|
||||
if extraInfo != "" {
|
||||
re := regexp.MustCompile(`tag: ([^,\)]+)`)
|
||||
tagMatch := re.FindStringSubmatch(extraInfo)
|
||||
if len(tagMatch) > 1 {
|
||||
tags = append(tags, tagMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
|
||||
// Any commit with multiple parents is a merge commit.
|
||||
// If there's a space then it means there must be more than one parent hash
|
||||
isMerge := strings.Contains(parentHashes, " ")
|
||||
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
IsMerge: isMerge,
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
||||
result := make([]*models.Commit, 0, len(commits))
|
||||
for i, commit := range commits {
|
||||
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
|
||||
result = append(result, commits[i:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode == "" {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = c.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
passedFirstPushedCommit := false
|
||||
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
|
||||
if err != nil {
|
||||
// must have no upstream branch so we'll consider everything as pushed
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
|
||||
cmd := c.getLogCmd(opts)
|
||||
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = c.setCommitMergedStatuses(opts.RefName, 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) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case REBASE_MODE_MERGING:
|
||||
return c.getNormalRebasingCommits()
|
||||
case REBASE_MODE_INTERACTIVE:
|
||||
return c.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "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 := []*models.Commit{}
|
||||
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "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([]*models.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() ([]*models.Commit, error) {
|
||||
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
|
||||
if err != nil {
|
||||
c.Log.Error(fmt.Sprintf("error occurred 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 := []*models.Commit{}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*models.Commit{{
|
||||
Sha: splitLine[1],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
Status: "rebasing",
|
||||
Action: splitLine[0],
|
||||
}}, commits...)
|
||||
}
|
||||
|
||||
return commits, 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) (*models.Commit, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
sha := strings.Split(lines[0], " ")[1]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
ancestor, err := c.getMergeBase(refName)
|
||||
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) getMergeBase(refName string) (string, error) {
|
||||
currentBranch, _, err := c.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
baseBranch := "master"
|
||||
if strings.HasPrefix(currentBranch, "feature/") {
|
||||
baseBranch = "develop"
|
||||
}
|
||||
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
func ignoringWarnings(commandOutput string) string {
|
||||
trimmedOutput := strings.TrimSpace(commandOutput)
|
||||
split := strings.Split(trimmedOutput, "\n")
|
||||
// need to get last line in case the first line is a warning about how the error is ambiguous.
|
||||
// At some point we should find a way to make it unambiguous
|
||||
lastLine := split[len(split)-1]
|
||||
|
||||
return lastLine
|
||||
}
|
||||
|
||||
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
|
||||
// all commits above this are deemed unpushed and marked as such.
|
||||
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
||||
limitFlag := ""
|
||||
if opts.Limit {
|
||||
limitFlag = "-300"
|
||||
}
|
||||
|
||||
filterFlag := ""
|
||||
if opts.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf(
|
||||
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
|
||||
opts.RefName,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
limitFlag,
|
||||
20,
|
||||
filterFlag,
|
||||
),
|
||||
)
|
||||
}
|
||||
114
pkg/commands/loading_commits_test.go
Normal file
114
pkg/commands/loading_commits_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
|
||||
func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
return &CommitListBuilder{
|
||||
Log: utils.NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return secureexec.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 secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah", 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 secureexec.Command("echo", "feature/test")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.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("HEAD"))
|
||||
})
|
||||
}
|
||||
}
|
||||
104
pkg/commands/loading_files.go
Normal file
104
pkg/commands/loading_files.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
// GetStatusFiles git status files
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*models.File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
if strings.HasPrefix(statusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", statusString)
|
||||
continue
|
||||
}
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
name := statusString[3:]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
previousName := ""
|
||||
if strings.Contains(name, RENAME_SEPARATOR) {
|
||||
split := strings.Split(name, RENAME_SEPARATOR)
|
||||
name = split[1]
|
||||
previousName = split[0]
|
||||
}
|
||||
|
||||
file := &models.File{
|
||||
Name: name,
|
||||
PreviousName: previousName,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
for i := 0; i < len(splitLines)-1; i++ {
|
||||
original := splitLines[i]
|
||||
if strings.HasPrefix(original, "R ") {
|
||||
next := splitLines[i+1]
|
||||
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
|
||||
splitLines[i] = updated
|
||||
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(splitLines, "\n"), nil
|
||||
}
|
||||
54
pkg/commands/loading_reflog_commits.go
Normal file
54
pkg/commands/loading_reflog_commits.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) <= 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
}
|
||||
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
|
||||
onlyObtainedNewReflogCommits = true
|
||||
// after this point we already have these reflogs loaded so we'll simply return the new ones
|
||||
return true, nil
|
||||
}
|
||||
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return commits, onlyObtainedNewReflogCommits, nil
|
||||
}
|
||||
60
pkg/commands/loading_remotes.go
Normal file
60
pkg/commands/loading_remotes.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
|
||||
// get remote branches
|
||||
unescaped := "git branch -r"
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goGitRemotes, err := c.Repo.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
remotes := make([]*models.Remote, len(goGitRemotes))
|
||||
for i, goGitRemote := range goGitRemotes {
|
||||
remoteName := goGitRemote.Config().Name
|
||||
|
||||
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
|
||||
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
|
||||
branches := make([]*models.RemoteBranch, len(matches))
|
||||
for j, match := range matches {
|
||||
branches[j] = &models.RemoteBranch{
|
||||
Name: match[1],
|
||||
RemoteName: remoteName,
|
||||
}
|
||||
}
|
||||
|
||||
remotes[i] = &models.Remote{
|
||||
Name: goGitRemote.Config().Name,
|
||||
Urls: goGitRemote.Config().URLs,
|
||||
Branches: branches,
|
||||
}
|
||||
}
|
||||
|
||||
// now lets sort our remotes by name alphabetically
|
||||
sort.Slice(remotes, func(i, j int) bool {
|
||||
// we want origin at the top because we'll be most likely to want it
|
||||
if remotes[i].Name == "origin" {
|
||||
return true
|
||||
}
|
||||
if remotes[j].Name == "origin" {
|
||||
return false
|
||||
}
|
||||
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
|
||||
})
|
||||
|
||||
return remotes, nil
|
||||
}
|
||||
65
pkg/commands/loading_stash.go
Normal file
65
pkg/commands/loading_stash.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
unescaped := "git stash list --pretty='%gs'"
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
stashEntries := []*models.StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
if filterPath == "" {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
stashEntries := []*models.StashEntry{}
|
||||
var currentStashEntry *models.StashEntry
|
||||
lines := utils.SplitLines(rawString)
|
||||
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
|
||||
re := regexp.MustCompile(`stash@\{(\d+)\}`)
|
||||
|
||||
outer:
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if !isAStash(lines[i]) {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatch(lines[i])
|
||||
idx, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = stashEntryFromLine(lines[i], idx)
|
||||
for i+1 < len(lines) && !isAStash(lines[i+1]) {
|
||||
i++
|
||||
if lines[i] == filterPath {
|
||||
stashEntries = append(stashEntries, currentStashEntry)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
return &models.StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
88
pkg/commands/loading_tags.go
Normal file
88
pkg/commands/loading_tags.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
|
||||
|
||||
func convertToInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := utils.TrimTrailingNewline(remoteBranchesStr)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
split := strings.Split(content, "\n")
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
|
||||
tags[i] = &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
// now lets sort our tags by name numerically
|
||||
re := regexp.MustCompile(semverRegex)
|
||||
|
||||
// the reason this is complicated is because we're both sorting alphabetically
|
||||
// and when we're dealing with semver strings
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
a := tags[i].Name
|
||||
b := tags[j].Name
|
||||
|
||||
matchA := re.FindStringSubmatch(a)
|
||||
matchB := re.FindStringSubmatch(b)
|
||||
|
||||
if len(matchA) > 0 && len(matchB) > 0 {
|
||||
numbersA := strings.Split(matchA[1], ".")
|
||||
numbersB := strings.Split(matchB[1], ".")
|
||||
k := 0
|
||||
for {
|
||||
if len(numbersA) == k && len(numbersB) == k {
|
||||
break
|
||||
}
|
||||
if len(numbersA) == k {
|
||||
return true
|
||||
}
|
||||
if len(numbersB) == k {
|
||||
return false
|
||||
}
|
||||
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
|
||||
return true
|
||||
}
|
||||
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
|
||||
return false
|
||||
}
|
||||
k++
|
||||
}
|
||||
|
||||
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
|
||||
}
|
||||
|
||||
return strings.ToLower(a) < strings.ToLower(b)
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
26
pkg/commands/models/branch.go
Normal file
26
pkg/commands/models/branch.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
// Branch : A git branch
|
||||
// duplicating this for now
|
||||
type Branch struct {
|
||||
Name string
|
||||
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
|
||||
DisplayName string
|
||||
Recency string
|
||||
Pushables string
|
||||
Pullables string
|
||||
UpstreamName string
|
||||
Head bool
|
||||
}
|
||||
|
||||
func (b *Branch) RefName() string {
|
||||
return b.Name
|
||||
}
|
||||
|
||||
func (b *Branch) ID() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
||||
func (b *Branch) Description() string {
|
||||
return b.RefName()
|
||||
}
|
||||
37
pkg/commands/models/commit.go
Normal file
37
pkg/commands/models/commit.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
Author string
|
||||
UnixTimestamp int64
|
||||
|
||||
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
|
||||
IsMerge bool
|
||||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
if len(c.Sha) < 8 {
|
||||
return c.Sha
|
||||
}
|
||||
return c.Sha[:8]
|
||||
}
|
||||
|
||||
func (c *Commit) RefName() string {
|
||||
return c.Sha
|
||||
}
|
||||
|
||||
func (c *Commit) ID() string {
|
||||
return c.RefName()
|
||||
}
|
||||
|
||||
func (c *Commit) Description() string {
|
||||
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
|
||||
}
|
||||
17
pkg/commands/models/commit_file.go
Normal file
17
pkg/commands/models/commit_file.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
// TODO: rename this to Path
|
||||
Name string
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
func (f *CommitFile) ID() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *CommitFile) Description() string {
|
||||
return f.Name
|
||||
}
|
||||
89
pkg/commands/models/file.go
Normal file
89
pkg/commands/models/file.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
// duplicating this for now
|
||||
type File struct {
|
||||
Name string
|
||||
PreviousName string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Added bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
DisplayString string
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
// sometimes we need to deal with either a node (which contains a file) or an actual file
|
||||
type IFile interface {
|
||||
GetHasUnstagedChanges() bool
|
||||
GetHasStagedChanges() bool
|
||||
GetIsTracked() bool
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
return f.PreviousName != ""
|
||||
}
|
||||
|
||||
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
|
||||
func (f *File) Names() []string {
|
||||
result := []string{f.Name}
|
||||
if f.PreviousName != "" {
|
||||
result = append(result, f.PreviousName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
func (f *File) Matches(f2 *File) bool {
|
||||
return utils.StringArraysOverlap(f.Names(), f2.Names())
|
||||
}
|
||||
|
||||
func (f *File) ID() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *File) Description() string {
|
||||
return f.Name
|
||||
}
|
||||
|
||||
func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
|
||||
return f.SubmoduleConfig(configs) != nil
|
||||
}
|
||||
|
||||
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||
for _, config := range configs {
|
||||
if f.Name == config.Name {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) GetHasUnstagedChanges() bool {
|
||||
return f.HasUnstagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetHasStagedChanges() bool {
|
||||
return f.HasStagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetIsTracked() bool {
|
||||
return f.Tracked
|
||||
}
|
||||
|
||||
func (f *File) GetPath() string {
|
||||
// TODO: remove concept of name; just use path
|
||||
return f.Name
|
||||
}
|
||||
20
pkg/commands/models/remote.go
Normal file
20
pkg/commands/models/remote.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
// Remote : A git remote
|
||||
type Remote struct {
|
||||
Name string
|
||||
Urls []string
|
||||
Branches []*RemoteBranch
|
||||
}
|
||||
|
||||
func (r *Remote) RefName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *Remote) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
func (r *Remote) Description() string {
|
||||
return r.RefName()
|
||||
}
|
||||
23
pkg/commands/models/remote_branch.go
Normal file
23
pkg/commands/models/remote_branch.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
// Remote Branch : A git remote branch
|
||||
type RemoteBranch struct {
|
||||
Name string
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) FullName() string {
|
||||
return r.RemoteName + "/" + r.Name
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) RefName() string {
|
||||
return r.FullName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
func (r *RemoteBranch) Description() string {
|
||||
return r.RefName()
|
||||
}
|
||||
21
pkg/commands/models/stash_entry.go
Normal file
21
pkg/commands/models/stash_entry.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
}
|
||||
|
||||
func (s *StashEntry) RefName() string {
|
||||
return fmt.Sprintf("stash@{%d}", s.Index)
|
||||
}
|
||||
|
||||
func (s *StashEntry) ID() string {
|
||||
return s.RefName()
|
||||
}
|
||||
|
||||
func (s *StashEntry) Description() string {
|
||||
return s.RefName() + ": " + s.Name
|
||||
}
|
||||
19
pkg/commands/models/submodule_config.go
Normal file
19
pkg/commands/models/submodule_config.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
type SubmoduleConfig struct {
|
||||
Name string
|
||||
Path string
|
||||
Url string
|
||||
}
|
||||
|
||||
func (r *SubmoduleConfig) RefName() string {
|
||||
return r.Name
|
||||
}
|
||||
|
||||
func (r *SubmoduleConfig) ID() string {
|
||||
return r.RefName()
|
||||
}
|
||||
|
||||
func (r *SubmoduleConfig) Description() string {
|
||||
return r.RefName()
|
||||
}
|
||||
18
pkg/commands/models/tag.go
Normal file
18
pkg/commands/models/tag.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
// Tag : A git tag
|
||||
type Tag struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (t *Tag) RefName() string {
|
||||
return t.Name
|
||||
}
|
||||
|
||||
func (t *Tag) ID() string {
|
||||
return t.RefName()
|
||||
}
|
||||
|
||||
func (t *Tag) Description() string {
|
||||
return "tag " + t.Name
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
os string
|
||||
shell string
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
openCommand string
|
||||
fallbackEscapedQuote string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
type OSCommand struct {
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getenv func(string) string
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
return &OSCommand{
|
||||
Log: log,
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
command: exec.Command,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getenv: os.Getenv,
|
||||
}
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
splitCmd := str.ToArgv(command)
|
||||
c.Log.Info(splitCmd)
|
||||
return sanitisedCommandOutput(
|
||||
c.command(splitCmd[0], splitCmd[1:]...).CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
// RunCommand runs a command and just returns the error
|
||||
func (c *OSCommand) RunCommand(command string) error {
|
||||
_, err := c.RunCommandWithOutput(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "other"
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return "directory"
|
||||
}
|
||||
return "file"
|
||||
}
|
||||
|
||||
// RunDirectCommand wrapper around direct commands
|
||||
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||
|
||||
return sanitisedCommandOutput(
|
||||
c.command(c.Platform.shell, c.Platform.shellArg, command).
|
||||
CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", err
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor, _ := c.getGlobalGitConfig("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.RunCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return c.PrepareSubProcess(editor, filename), nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
return c.command(cmdName, commandArgs...)
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.escapedQuote
|
||||
if strings.Contains(message, c.Platform.escapedQuote) {
|
||||
escapedQuote = c.Platform.fallbackEscapedQuote
|
||||
}
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
return err
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: runtime.GOOS,
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
openCommand: "open {{filename}}",
|
||||
fallbackEscapedQuote: "\"",
|
||||
}
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func newDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(newDummyLog(), newDummyAppConfig())
|
||||
}
|
||||
|
||||
func newDummyAppConfig() *config.AppConfig {
|
||||
appConfig := &config.AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
Commit: "",
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: viper.New(),
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
}
|
||||
|
||||
func TestOSCommandRunCommandWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"echo -n '123'",
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(output string, err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(newDummyOSCommand().RunCommandWithOutput(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandRunCommand(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"rmdir unexisting-folder",
|
||||
func(err error) {
|
||||
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(newDummyOSCommand().RunCommand(s.command))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandOpenFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"test"}, arg)
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := newDummyOSCommand()
|
||||
OSCmd.command = s.command
|
||||
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandEditFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
test func(*exec.Cmd, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "emacs", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "vi", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := newDummyOSCommand()
|
||||
OSCmd.command = s.command
|
||||
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
|
||||
OSCmd.getenv = s.getenv
|
||||
|
||||
s.test(OSCmd.EditFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
|
||||
func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
|
||||
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
osCommand.Platform.os = "linux"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
osCommand := newDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
|
||||
expected := "hello test"
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
setup func()
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"testFile",
|
||||
func() {
|
||||
if _, err := os.Create("testFile"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file with spaces",
|
||||
func() {
|
||||
if _, err := os.Create("file with spaces"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"testDirectory",
|
||||
func() {
|
||||
if err := os.Mkdir("testDirectory", 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "directory", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"nonExistant",
|
||||
func() {},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "other", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.setup()
|
||||
s.test(newDummyOSCommand().FileType(s.path))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package commands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: "windows",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: `\"`,
|
||||
fallbackEscapedQuote: "\\'",
|
||||
}
|
||||
}
|
||||
137
pkg/commands/oscommands/copy.go
Normal file
137
pkg/commands/oscommands/copy.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
/* MIT License
|
||||
*
|
||||
* Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// CopyFile copies the contents of the file named src to the file named
|
||||
// by dst. The file will be created if it does not already exist. If the
|
||||
// destination file exists, all it's contents will be replaced by the contents
|
||||
// of the source file. The file mode will be copied from the source and
|
||||
// the copied data is synced/flushed to stable storage.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if e := out.Close(); e != nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = out.Sync()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
si, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.Chmod(dst, si.Mode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
|
||||
// Source directory must exist. If destination already exists we'll clobber it.
|
||||
// Symlinks are ignored and skipped.
|
||||
func CopyDir(src string, dst string) (err error) {
|
||||
src = filepath.Clean(src)
|
||||
dst = filepath.Clean(dst)
|
||||
|
||||
si, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !si.IsDir() {
|
||||
return fmt.Errorf("source is not a directory")
|
||||
}
|
||||
|
||||
_, err = os.Stat(dst)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
// it exists so let's remove it
|
||||
if err := os.RemoveAll(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(dst, si.Mode())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := ioutil.ReadDir(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
err = CopyDir(srcPath, dstPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Skip symlinks.
|
||||
if entry.Mode()&os.ModeSymlink != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err = CopyFile(srcPath, dstPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
11
pkg/commands/oscommands/dummies.go
Normal file
11
pkg/commands/oscommands/dummies.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
|
||||
}
|
||||
97
pkg/commands/oscommands/exec_live_default.go
Normal file
97
pkg/commands/oscommands/exec_live_default.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
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 utils.Safe(func() {
|
||||
scanner := bufio.NewScanner(ptmx)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
toOutput := strings.Trim(scanner.Text(), " ")
|
||||
_, _ = ptmx.WriteString(output(toOutput))
|
||||
}
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
ptmx.Close()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
9
pkg/commands/oscommands/exec_live_win.go
Normal file
9
pkg/commands/oscommands/exec_live_win.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
// 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)
|
||||
}
|
||||
472
pkg/commands/oscommands/os.go
Normal file
472
pkg/commands/oscommands/os.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
type OSCommand struct {
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
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: secureexec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
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
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||
c.BeforeExecuteCmd = cmd
|
||||
}
|
||||
|
||||
type RunCommandOptions struct {
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
|
||||
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
|
||||
_, err := c.RunCommandWithOutputWithOptions(command, options)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
|
||||
// however there's a bizarre compiler error/warning when you pass in a formatString
|
||||
// with a percent sign because it thinks it's supposed to be a formatString when
|
||||
// in that case it's not. To get around that error you'll need to define the string
|
||||
// in a variable and pass the variable into RunCommandWithOutput.
|
||||
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
command := formatString
|
||||
if formatArgs != nil {
|
||||
command = fmt.Sprintf(formatString, formatArgs...)
|
||||
}
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", command).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
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)
|
||||
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
|
||||
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
|
||||
ttyText = ttyText + " " + word
|
||||
|
||||
prompts := map[string]string{
|
||||
`.+'s password:`: "password",
|
||||
`Password\s*for\s*'.+':`: "password",
|
||||
`Username\s*for\s*'.+':`: "username",
|
||||
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// RunCommand runs a command and just returns the error
|
||||
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
|
||||
// need access to the shell
|
||||
func (c *OSCommand) RunShellCommand(command string) error {
|
||||
c.Log.WithField("command", command).Info("RunShellCommand")
|
||||
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "other"
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return "directory"
|
||||
}
|
||||
return "file"
|
||||
}
|
||||
|
||||
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 "", utils.WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().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().OS.OpenLinkCommand
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
cmd := c.Command(cmdName, commandArgs...)
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PrepareShellSubProcess returns the pointer to a custom command
|
||||
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// 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 utils.WrapError(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
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 "", utils.WrapError(err)
|
||||
}
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
}
|
||||
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
c.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
c.Log.Error(err)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a file or directory at the specified path
|
||||
func (c *OSCommand) Remove(filename string) error {
|
||||
err := os.RemoveAll(filename)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
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) + `"`
|
||||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
|
||||
for i, str := range commandStrings {
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
}
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmds[i+1].Stdin = stdout
|
||||
}
|
||||
|
||||
// keeping this here in case I adapt this code for some other purpose in the future
|
||||
// cmds[len(cmds)-1].Stdout = os.Stdout
|
||||
|
||||
finalErrors := []string{}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(cmds))
|
||||
|
||||
for _, cmd := range cmds {
|
||||
currentCmd := cmd
|
||||
go utils.Safe(func() {
|
||||
stderr, err := currentCmd.StderrPipe()
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
if err := currentCmd.Start(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
if b, err := ioutil.ReadAll(stderr); err == nil {
|
||||
if len(b) > 0 {
|
||||
finalErrors = append(finalErrors, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
if err := currentCmd.Wait(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(finalErrors) > 0 {
|
||||
return errors.New(strings.Join(finalErrors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Kill(cmd *exec.Cmd) error {
|
||||
if cmd.Process == nil {
|
||||
// somebody got to it before we were able to, poor bastard
|
||||
return nil
|
||||
}
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
19
pkg/commands/oscommands/os_default_platform.go
Normal file
19
pkg/commands/oscommands/os_default_platform.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: []string{"cat"},
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
252
pkg/commands/oscommands/os_test.go
Normal file
252
pkg/commands/oscommands/os_test.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"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 secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.OS = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
|
||||
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.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandFileType is a function.
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
setup func()
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"testFile",
|
||||
func() {
|
||||
if _, err := os.Create("testFile"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file with spaces",
|
||||
func() {
|
||||
if _, err := os.Create("file with spaces"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "file", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"testDirectory",
|
||||
func() {
|
||||
if err := os.Mkdir("testDirectory", 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "directory", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"nonExistant",
|
||||
func() {},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "other", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.setup()
|
||||
s.test(NewDummyOSCommand().FileType(s.path))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandCreateTempFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
content string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"filename",
|
||||
"content",
|
||||
func(path string, err error) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "content", string(content))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
|
||||
})
|
||||
}
|
||||
}
|
||||
11
pkg/commands/oscommands/os_windows.go
Normal file
11
pkg/commands/oscommands/os_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: []string{"cmd", "/c", "type"},
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
}
|
||||
}
|
||||
142
pkg/commands/patch/hunk.go
Normal file
142
pkg/commands/patch/hunk.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type PatchHunk struct {
|
||||
FirstLineIdx int
|
||||
oldStart int
|
||||
newStart int
|
||||
heading string
|
||||
bodyLines []string
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) LastLineIdx() int {
|
||||
return hunk.FirstLineIdx + len(hunk.bodyLines)
|
||||
}
|
||||
|
||||
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
|
||||
header := lines[0]
|
||||
bodyLines := lines[1:]
|
||||
|
||||
oldStart, newStart, heading := headerInfo(header)
|
||||
|
||||
return &PatchHunk{
|
||||
oldStart: oldStart,
|
||||
newStart: newStart,
|
||||
heading: heading,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
bodyLines: bodyLines,
|
||||
}
|
||||
}
|
||||
|
||||
func headerInfo(header string) (int, int, string) {
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(header)
|
||||
|
||||
oldStart := utils.MustConvertToInt(match[1])
|
||||
newStart := utils.MustConvertToInt(match[2])
|
||||
heading := match[3]
|
||||
|
||||
return oldStart, newStart, heading
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
|
||||
skippedNewlineMessageIndex := -1
|
||||
newLines := []string{}
|
||||
|
||||
lineIdx := hunk.FirstLineIdx
|
||||
for _, line := range hunk.bodyLines {
|
||||
lineIdx++ // incrementing at the start to skip the header line
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
|
||||
|
||||
firstChar, content := line[:1], line[1:]
|
||||
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
|
||||
|
||||
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
|
||||
newLines = append(newLines, transformedFirstChar+content)
|
||||
continue
|
||||
}
|
||||
|
||||
if transformedFirstChar == "+" {
|
||||
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
|
||||
skippedNewlineMessageIndex = lineIdx + 1
|
||||
}
|
||||
}
|
||||
|
||||
return newLines
|
||||
}
|
||||
|
||||
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
|
||||
if reverse {
|
||||
if !isLineSelected && firstChar == "+" {
|
||||
return " "
|
||||
} else if firstChar == "-" {
|
||||
return "+"
|
||||
} else if firstChar == "+" {
|
||||
return "-"
|
||||
} else {
|
||||
return firstChar
|
||||
}
|
||||
}
|
||||
|
||||
if !isLineSelected && firstChar == "-" {
|
||||
return " "
|
||||
}
|
||||
|
||||
return firstChar
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
|
||||
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
|
||||
bodyLines := hunk.updatedLines(lineIndices, reverse)
|
||||
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
|
||||
if !ok {
|
||||
return startOffset, ""
|
||||
}
|
||||
return startOffset, header + strings.Join(bodyLines, "")
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
|
||||
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
|
||||
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
|
||||
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
|
||||
|
||||
if changeCount == 0 {
|
||||
// if nothing has changed we just return nothing
|
||||
return startOffset, "", false
|
||||
}
|
||||
|
||||
var oldStart int
|
||||
if reverse {
|
||||
oldStart = hunk.newStart
|
||||
} else {
|
||||
oldStart = hunk.oldStart
|
||||
}
|
||||
|
||||
var newStartOffset int
|
||||
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
||||
// if the hunk went from positive to zero length, we need to decrement the starting point by one
|
||||
if oldLength == 0 {
|
||||
newStartOffset = 1
|
||||
} else if newLength == 0 {
|
||||
newStartOffset = -1
|
||||
} else {
|
||||
newStartOffset = 0
|
||||
}
|
||||
|
||||
newStart := oldStart + startOffset + newStartOffset
|
||||
|
||||
newStartOffset = startOffset + newLength - oldLength
|
||||
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
|
||||
return newStartOffset, formattedHeader, true
|
||||
}
|
||||
308
pkg/commands/patch/patch_manager.go
Normal file
308
pkg/commands/patch/patch_manager.go
Normal file
@@ -0,0 +1,308 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchStatus int
|
||||
|
||||
const (
|
||||
// UNSELECTED is for when the commit file has not been added to the patch in any way
|
||||
UNSELECTED PatchStatus = iota
|
||||
// WHOLE is for when you want to add the whole diff of a file to the patch,
|
||||
// including e.g. if it was deleted
|
||||
WHOLE
|
||||
// PART is for when you're only talking about specific lines that have been modified
|
||||
PART
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
mode PatchStatus
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
}
|
||||
|
||||
type applyPatchFunc func(patch string, flags ...string) error
|
||||
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
|
||||
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
|
||||
type PatchManager struct {
|
||||
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
|
||||
To string
|
||||
From string
|
||||
Reverse bool
|
||||
|
||||
// CanRebase tells us whether we're allowed to modify our commits. CanRebase should be true for commits of the currently checked out branch and false for everything else
|
||||
// TODO: move this out into a proper mode struct in the gui package: it doesn't really belong here
|
||||
CanRebase bool
|
||||
|
||||
// fileInfoMap starts empty but you add files to it as you go along
|
||||
fileInfoMap map[string]*fileInfo
|
||||
Log *logrus.Entry
|
||||
ApplyPatch applyPatchFunc
|
||||
|
||||
// LoadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
|
||||
LoadFileDiff loadFileDiffFunc
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
|
||||
return &PatchManager{
|
||||
Log: log,
|
||||
ApplyPatch: applyPatch,
|
||||
LoadFileDiff: loadFileDiff,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.Reverse = reverse
|
||||
p.CanRebase = canRebase
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) addFileWhole(info *fileInfo) {
|
||||
info.mode = WHOLE
|
||||
lineCount := len(strings.Split(info.diff, "\n"))
|
||||
info.includedLineIndices = make([]int, lineCount)
|
||||
// add every line index
|
||||
for i := 0; i < lineCount; i++ {
|
||||
info.includedLineIndices[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) removeFile(info *fileInfo) {
|
||||
info.mode = UNSELECTED
|
||||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIndicesForRange(first, last int) []int {
|
||||
indices := []int{}
|
||||
for i := first; i <= last; i++ {
|
||||
indices = append(indices, i)
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
diff, err := p.LoadFileDiff(p.From, p.To, p.Reverse, filename, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
}
|
||||
|
||||
p.fileInfoMap[filename] = info
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
p.removeFile(info)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return ""
|
||||
}
|
||||
|
||||
switch info.mode {
|
||||
case WHOLE:
|
||||
// use the whole diff
|
||||
// the reverse flag is only for part patches so we're ignoring it here
|
||||
return info.diff
|
||||
case PART:
|
||||
// generate a new diff with just the selected lines
|
||||
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
|
||||
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
parser, err := NewPatchParser(p.Log, patch)
|
||||
if err != nil {
|
||||
// swallowing for now
|
||||
return ""
|
||||
}
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
|
||||
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
|
||||
// sort files by name then iterate through and render each patch
|
||||
filenames := make([]string, len(p.fileInfoMap))
|
||||
index := 0
|
||||
for filename := range p.fileInfoMap {
|
||||
filenames[index] = filename
|
||||
index++
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
output := []string{}
|
||||
for _, filename := range filenames {
|
||||
patch := p.RenderPatchForFile(filename, plain, false, true)
|
||||
if patch != "" {
|
||||
output = append(output, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
result := ""
|
||||
for _, patch := range p.renderEachFilePatch(plain) {
|
||||
if patch != "" {
|
||||
result += patch + "\n"
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
if parent != p.To {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if !ok {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info.includedLineIndices, nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ApplyPatches(reverse bool) error {
|
||||
// for whole patches we'll apply the patch in reverse
|
||||
// but for part patches we'll apply a reverse patch forwards
|
||||
for filename, info := range p.fileInfoMap {
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
|
||||
applyFlags := []string{"index", "3way"}
|
||||
reverseOnGenerate := false
|
||||
if reverse {
|
||||
if info.mode == WHOLE {
|
||||
applyFlags = append(applyFlags, "reverse")
|
||||
} else {
|
||||
reverseOnGenerate = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
// first run we try with the original header, then without
|
||||
for _, keepOriginalHeader := range []bool{true, false} {
|
||||
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
|
||||
if patch == "" {
|
||||
continue
|
||||
}
|
||||
if err = p.ApplyPatch(patch, applyFlags...); err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clears the patch
|
||||
func (p *PatchManager) Reset() {
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) Active() bool {
|
||||
return p.To != ""
|
||||
}
|
||||
|
||||
func (p *PatchManager) IsEmpty() bool {
|
||||
for _, fileInfo := range p.fileInfoMap {
|
||||
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// if any of these things change we'll need to reset and start a new patch
|
||||
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
|
||||
return from != p.From || to != p.To || reverse != p.Reverse
|
||||
}
|
||||
158
pkg/commands/patch/patch_modifier.go
Normal file
158
pkg/commands/patch/patch_modifier.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
if len(match) <= 1 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||
hunks := []*PatchHunk{}
|
||||
firstLineIdx := -1
|
||||
var hunkLines []string
|
||||
pastDiffHeader := false
|
||||
|
||||
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
|
||||
isHunkHeader := strings.HasPrefix(line, "@@ -")
|
||||
|
||||
if isHunkHeader {
|
||||
if pastDiffHeader { // we need to persist the current hunk
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
pastDiffHeader = true
|
||||
firstLineIdx = lineIdx
|
||||
hunkLines = []string{line}
|
||||
continue
|
||||
}
|
||||
|
||||
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
|
||||
continue
|
||||
}
|
||||
|
||||
hunkLines = append(hunkLines, line)
|
||||
}
|
||||
|
||||
if pastDiffHeader {
|
||||
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
|
||||
}
|
||||
|
||||
return hunks
|
||||
}
|
||||
|
||||
type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
filename string
|
||||
hunks []*PatchHunk
|
||||
header string
|
||||
}
|
||||
|
||||
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: log,
|
||||
filename: filename,
|
||||
hunks: GetHunksFromDiff(diffText),
|
||||
header: GetHeaderFromDiff(diffText),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
// step one is getting only those hunks which we care about
|
||||
hunksInRange := []*PatchHunk{}
|
||||
outer:
|
||||
for _, hunk := range d.hunks {
|
||||
// if there is any line in our lineIndices array that the hunk contains, we append it
|
||||
for _, lineIdx := range lineIndices {
|
||||
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
|
||||
hunksInRange = append(hunksInRange, hunk)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// step 2 is collecting all the hunks with new headers
|
||||
startOffset := 0
|
||||
formattedHunks := ""
|
||||
var formattedHunk string
|
||||
for _, hunk := range hunksInRange {
|
||||
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
|
||||
formattedHunks += formattedHunk
|
||||
}
|
||||
|
||||
if formattedHunks == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var fileHeader string
|
||||
// for staging/unstaging lines we don't want the original header because
|
||||
// it makes git confused e.g. when dealing with deleted/added files
|
||||
// but with building and applying patches the original header gives git
|
||||
// information it needs to cleanly apply patches
|
||||
if keepOriginalHeader {
|
||||
fileHeader = d.header
|
||||
} else {
|
||||
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
|
||||
}
|
||||
|
||||
return fileHeader + formattedHunks
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
// generate array of consecutive line indices from our range
|
||||
selectedLines := []int{}
|
||||
for i := firstLineIdx; i <= lastLineIdx; i++ {
|
||||
selectedLines = append(selectedLines, i)
|
||||
}
|
||||
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func (d *PatchModifier) OriginalPatchLength() int {
|
||||
if len(d.hunks) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return d.hunks[len(d.hunks)-1].LastLineIdx()
|
||||
}
|
||||
|
||||
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
// I want to know, given a hunk, what line a given index is on
|
||||
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
|
||||
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
|
||||
|
||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||
|
||||
return hunk.newStart + offset
|
||||
}
|
||||
|
||||
func nLinesWithPrefix(lines []string, chars []string) int {
|
||||
result := 0
|
||||
for _, line := range lines {
|
||||
for _, char := range chars {
|
||||
if line[:1] == char {
|
||||
result++
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
548
pkg/commands/patch/patch_modifier_test.go
Normal file
548
pkg/commands/patch/patch_modifier_test.go
Normal file
@@ -0,0 +1,548 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const simpleDiff = `diff --git a/filename b/filename
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const addNewlineToEndOfFile = `diff --git a/filename b/filename
|
||||
index 80a73f1..e48a11c 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
\ No newline at end of file
|
||||
+last line
|
||||
`
|
||||
|
||||
const removeNewlinefromEndOfFile = `diff --git a/filename b/filename
|
||||
index e48a11c..80a73f1 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`
|
||||
|
||||
const twoHunks = `diff --git a/filename b/filename
|
||||
index e48a11c..b2ab81b 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +8,8 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
+lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const newFile = `diff --git a/newfile b/newfile
|
||||
new file mode 100644
|
||||
index 0000000..4e680cc
|
||||
--- /dev/null
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,3 @@
|
||||
+apple
|
||||
+orange
|
||||
+grape
|
||||
`
|
||||
|
||||
const addNewlineToPreviouslyEmptyFile = `diff --git a/newfile b/newfile
|
||||
index e69de29..c6568ea 100644
|
||||
--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`
|
||||
|
||||
const exampleHunk = `@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
// TestModifyPatchForRange is a function.
|
||||
func TestModifyPatchForRange(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
diffText string
|
||||
firstLineIndex int
|
||||
lastLineIndex int
|
||||
reverse bool
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "nothing selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: -1,
|
||||
lastLineIndex: -1,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "only context selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 5,
|
||||
lastLineIndex: 5,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "whole range selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "only removal selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "only addition selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "range that extends beyond diff bounds",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "whole range reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
+orange
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
+orange
|
||||
grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
\ No newline at end of file
|
||||
+last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, addition only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,5 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, removal only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 10,
|
||||
lastLineIndex: 10,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,3 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, removal only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,3 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, addition only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,5 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging two whole hunks",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +8,8 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
+lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging part of both hunks",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +9,7 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging part of both hunks, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: true,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,8 +7,7 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-pear
|
||||
lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,3 @@
|
||||
+apple
|
||||
+orange
|
||||
+grape
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding part of a new file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,2 @@
|
||||
+apple
|
||||
+orange
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new file, reversed",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,3 +0,0 @@
|
||||
-apple
|
||||
-orange
|
||||
-grape
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new line to a previously empty file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new line to a previously empty file, reversed",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,1 +0,0 @@
|
||||
-new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineNumberOfLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
hunk *PatchHunk
|
||||
idx int
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "nothing selected",
|
||||
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
|
||||
idx: 15,
|
||||
expected: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := s.hunk.LineNumberOfLine(s.idx)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
215
pkg/commands/patch/patch_parser.go
Normal file
215
pkg/commands/patch/patch_parser.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchLineKind int
|
||||
|
||||
const (
|
||||
PATCH_HEADER PatchLineKind = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
ADDITION
|
||||
DELETION
|
||||
CONTEXT
|
||||
NEWLINE_MESSAGE
|
||||
)
|
||||
|
||||
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
|
||||
|
||||
type PatchLine struct {
|
||||
Kind PatchLineKind
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
type PatchParser struct {
|
||||
Log *logrus.Entry
|
||||
PatchLines []*PatchLine
|
||||
PatchHunks []*PatchHunk
|
||||
HunkStarts []int
|
||||
StageableLines []int // rename to mention we're talking about indexes
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
return &PatchParser{
|
||||
Log: log,
|
||||
HunkStarts: hunkStarts, // deprecated
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
// which contains the line index, then returns the hunk considering the offset.
|
||||
// e.g. if the offset is 1 it will return the next hunk.
|
||||
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
|
||||
if len(p.PatchHunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for index, hunk := range p.PatchHunks {
|
||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
|
||||
resultIndex := index + offset
|
||||
if resultIndex < 0 {
|
||||
resultIndex = 0
|
||||
} else if resultIndex > len(p.PatchHunks)-1 {
|
||||
resultIndex = len(p.PatchHunks) - 1
|
||||
}
|
||||
return p.PatchHunks[resultIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// if your cursor is past the last hunk, select the last hunk
|
||||
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
|
||||
return p.PatchHunks[len(p.PatchHunks)-1]
|
||||
}
|
||||
|
||||
// otherwise select the first
|
||||
return p.PatchHunks[0]
|
||||
}
|
||||
|
||||
// selected means you've got it highlighted with your cursor
|
||||
// included means the line has been included in the patch (only applicable when
|
||||
// building a patch)
|
||||
func (l *PatchLine) render(selected bool, included bool) string {
|
||||
content := l.Content
|
||||
if len(content) == 0 {
|
||||
content = " " // using the space so that we can still highlight if necessary
|
||||
}
|
||||
|
||||
// for hunk headers we need to start off cyan and then use white for the message
|
||||
if l.Kind == HUNK_HEADER {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
match := re.FindStringSubmatch(content)
|
||||
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
}
|
||||
|
||||
var colorAttr color.Attribute
|
||||
switch l.Kind {
|
||||
case PATCH_HEADER:
|
||||
colorAttr = color.Bold
|
||||
case ADDITION:
|
||||
colorAttr = color.FgGreen
|
||||
case DELETION:
|
||||
colorAttr = color.FgRed
|
||||
case COMMIT_SHA:
|
||||
colorAttr = color.FgYellow
|
||||
default:
|
||||
colorAttr = theme.DefaultTextColor
|
||||
}
|
||||
|
||||
return coloredString(colorAttr, content, selected, included)
|
||||
}
|
||||
|
||||
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
if selected {
|
||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
if included {
|
||||
clIncluded = color.New(append(attributes, color.BgGreen)...)
|
||||
} else {
|
||||
clIncluded = color.New(attributes...)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return utils.ColoredStringDirect(str, clIncluded)
|
||||
}
|
||||
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind PatchLineKind
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
if len(line) > 0 {
|
||||
firstChar = line[:1]
|
||||
}
|
||||
if index == 0 && strings.HasPrefix(line, "commit") {
|
||||
lineKind = COMMIT_SHA
|
||||
pastCommitDescription = false
|
||||
} else if !pastCommitDescription {
|
||||
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
|
||||
pastCommitDescription = true
|
||||
lineKind = PATCH_HEADER
|
||||
} else {
|
||||
lineKind = COMMIT_DESCRIPTION
|
||||
}
|
||||
} else if firstChar == "@" {
|
||||
pastFirstHunkHeader = true
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
lineKind = HUNK_HEADER
|
||||
} else if pastFirstHunkHeader {
|
||||
switch firstChar {
|
||||
case "-":
|
||||
lineKind = DELETION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "+":
|
||||
lineKind = ADDITION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "\\":
|
||||
lineKind = NEWLINE_MESSAGE
|
||||
case " ":
|
||||
lineKind = CONTEXT
|
||||
}
|
||||
} else {
|
||||
lineKind = PATCH_HEADER
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines, nil
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
|
||||
renderedLines := make([]string, len(p.PatchLines))
|
||||
for index, patchLine := range p.PatchLines {
|
||||
selected := index >= firstLineIndex && index <= lastLineIndex
|
||||
included := utils.IncludesInt(incLineIndices, index)
|
||||
renderedLines[index] = patchLine.render(selected, included)
|
||||
}
|
||||
result := strings.Join(renderedLines, "\n")
|
||||
if strings.TrimSpace(utils.Decolorise(result)) == "" {
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||
// note this will actually include the current index if it is stageable
|
||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||
for _, lineIndex := range p.StageableLines {
|
||||
if lineIndex >= currentIndex {
|
||||
return lineIndex
|
||||
}
|
||||
}
|
||||
return p.StageableLines[len(p.StageableLines)-1]
|
||||
}
|
||||
231
pkg/commands/patch_rebases.go
Normal file
231
pkg/commands/patch_rebases.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int, p *patch.PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// time to amend the selected commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error {
|
||||
if sourceCommitIdx < destinationCommitIdx {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch forward
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
if len(commits)-1 < sourceCommitIdx {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
todo := ""
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
a := "pick"
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
a = "edit"
|
||||
}
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the source commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// now we should be up to the destination, so let's apply forward these patches to that.
|
||||
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if stash {
|
||||
if err := c.StashDo(0, "apply"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int, p *patch.PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
164
pkg/commands/pull_request.go
Normal file
164
pkg/commands/pull_request.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
PullRequestURL string
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func getServices(config config.AppConfigurer) []*Service {
|
||||
services := []*Service{
|
||||
NewService("github", "github.com", "github.com"),
|
||||
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
|
||||
NewService("gitlab", "gitlab.com", "gitlab.com"),
|
||||
}
|
||||
|
||||
configServices := config.GetUserConfig().Services
|
||||
|
||||
for repoDomain, typeAndDomain := range configServices {
|
||||
splitData := strings.Split(typeAndDomain, ":")
|
||||
if len(splitData) != 2 {
|
||||
// TODO log this misconfiguration
|
||||
continue
|
||||
}
|
||||
|
||||
service := NewService(splitData[0], repoDomain, splitData[1])
|
||||
if service == nil {
|
||||
// TODO log this unsupported service
|
||||
continue
|
||||
}
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// NewPullRequest creates new instance of PullRequest
|
||||
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
return &PullRequest{
|
||||
GitServices: getServices(gitCommand.Config),
|
||||
GitCommand: gitCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) error {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return "", errors.New(pr.GitCommand.Tr.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.UnsupportedGitService)
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
pullRequestURL := fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
isHTTP := strings.HasPrefix(url, "http")
|
||||
|
||||
if isHTTP {
|
||||
splits := strings.Split(url, "/")
|
||||
owner := strings.Join(splits[3:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
tmpSplit := strings.Split(url, ":")
|
||||
splits := strings.Split(tmpSplit[1], "/")
|
||||
owner := strings.Join(splits[0:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
173
pkg/commands/pull_request_test.go
Normal file
173
pkg/commands/pull_request_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"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 *models.Branch
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/ui",
|
||||
},
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/divide-operation",
|
||||
},
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.getGitConfigValue = func(path string) (string, error) {
|
||||
assert.Equal(t, path, "remote.origin.url")
|
||||
return s.remoteUrl, nil
|
||||
}
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
}
|
||||
}
|
||||
287
pkg/commands/rebasing.go
Normal file
287
pkg/commands/rebasing.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.PrepareInteractiveRebaseCommand(sha, todo, false)
|
||||
}
|
||||
|
||||
func (c *GitCommand) MoveCommitDown(commits []*models.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.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 []*models.Commit, index int, action string) error {
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(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() {
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
|
||||
c.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
splitCmd := str.ToArgv(cmdStr)
|
||||
|
||||
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, "GIT_EDITOR="+ex)
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
|
||||
baseIndex := actionIndex + 1
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit)
|
||||
}
|
||||
|
||||
if action == "squash" || action == "fixup" {
|
||||
baseIndex++
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(c.Tr.CannotSquashOntoSecondCommit)
|
||||
}
|
||||
}
|
||||
|
||||
todo := ""
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
var commitAction string
|
||||
if i == actionIndex {
|
||||
commitAction = action
|
||||
} else if commit.IsMerge {
|
||||
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
|
||||
// doing this means we don't need to worry about rebasing over merges which always causes problems.
|
||||
// you typically shouldn't be doing rebases that pass over merge commits anyway.
|
||||
commitAction = "drop"
|
||||
} else {
|
||||
commitAction = "pick"
|
||||
}
|
||||
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
return todo, commits[baseIndex].Sha, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
func (c *GitCommand) AmendTo(sha string) error {
|
||||
if err := c.CreateFixupCommit(sha); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SquashAllAboveFixupCommits(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 := filepath.Join(c.DotGitDir, "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 := filepath.Join(c.DotGitDir, "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)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
|
||||
return c.runSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --autostash --autosquash %s^",
|
||||
sha,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
|
||||
// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")`
|
||||
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
|
||||
if len(commits)-1 < commitIndex {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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) GenericMergeOrRebaseAction(commandType string, command string) error {
|
||||
err := c.runSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git %s --%s",
|
||||
commandType,
|
||||
command,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "no rebase in progress") {
|
||||
return err
|
||||
}
|
||||
c.Log.Warn(err)
|
||||
}
|
||||
|
||||
// sometimes we need to do a sequence of things in a rebase but the user needs to
|
||||
// fix merge conflicts along the way. When this happens we queue up the next step
|
||||
// so that after the next successful rebase continue we can continue from where we left off
|
||||
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
|
||||
f := c.onSuccessfulContinue
|
||||
c.onSuccessfulContinue = nil
|
||||
return f()
|
||||
}
|
||||
if command == "abort" {
|
||||
c.onSuccessfulContinue = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) runSkipEditorCommand(command string) error {
|
||||
cmd := c.OSCommand.ExecutableFromString(command)
|
||||
lazyGitPath := c.OSCommand.GetLazygitPath()
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
)
|
||||
return c.OSCommand.RunExecutable(cmd)
|
||||
}
|
||||
43
pkg/commands/remotes.go
Normal file
43
pkg/commands/remotes.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.RunCommand("git remote add %s %s", name, url)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
return c.GetConfigValue("remote.origin.url")
|
||||
}
|
||||
58
pkg/commands/stash_entries.go
Normal file
58
pkg/commands/stash_entries.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
|
||||
}
|
||||
|
||||
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
|
||||
if err := c.RunCommand("git stash --keep-index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.StashSave(message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if you had staged an untracked file, that will now appear as 'AD' in git status
|
||||
// meaning it's deleted in your working tree but added in your index. Given that it's
|
||||
// now safely stashed, we need to remove it.
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Names(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package commands
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
func (s *StashEntry) GetDisplayStrings() []string {
|
||||
return []string{s.DisplayString}
|
||||
}
|
||||
55
pkg/commands/status.go
Normal file
55
pkg/commands/status.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
REBASE_MODE_NORMAL = "normal"
|
||||
REBASE_MODE_INTERACTIVE = "interactive"
|
||||
REBASE_MODE_REBASING = "rebasing"
|
||||
REBASE_MODE_MERGING = "merging"
|
||||
)
|
||||
|
||||
// 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(filepath.Join(c.DotGitDir, "rebase-apply"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
return REBASE_MODE_NORMAL, nil
|
||||
}
|
||||
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
|
||||
if exists {
|
||||
return REBASE_MODE_INTERACTIVE, err
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorkingTreeState() string {
|
||||
rebaseMode, _ := c.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
return REBASE_MODE_REBASING
|
||||
}
|
||||
merging, _ := c.IsInMergeState()
|
||||
if merging {
|
||||
return REBASE_MODE_MERGING
|
||||
}
|
||||
return REBASE_MODE_NORMAL
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD"))
|
||||
}
|
||||
|
||||
func (c *GitCommand) IsBareRepo() bool {
|
||||
// note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git
|
||||
_, err := c.Repo.Worktree()
|
||||
return err == gogit.ErrIsBareRepository
|
||||
}
|
||||
165
pkg/commands/submodules.go
Normal file
165
pkg/commands/submodules.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
// .gitmodules looks like this:
|
||||
// [submodule "mysubmodule"]
|
||||
// path = blah/mysubmodule
|
||||
// url = git@github.com:subbo.git
|
||||
|
||||
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
|
||||
file, err := os.Open(".gitmodules")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
firstMatch := func(str string, regex string) (string, bool) {
|
||||
re := regexp.MustCompile(regex)
|
||||
matches := re.FindStringSubmatch(str)
|
||||
|
||||
if len(matches) > 0 {
|
||||
return matches[1], true
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
configs := []*models.SubmoduleConfig{}
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok {
|
||||
configs = append(configs, &models.SubmoduleConfig{Name: name})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(configs) > 0 {
|
||||
lastConfig := configs[len(configs)-1]
|
||||
|
||||
if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok {
|
||||
lastConfig.Path = path
|
||||
} else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok {
|
||||
lastConfig.Url = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
||||
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
|
||||
// because the intention here is to have no dirty worktree state
|
||||
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
|
||||
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
|
||||
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
// not doing an --init here because the user probably doesn't want that
|
||||
return c.RunCommand("git submodule update --force")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there's an error here about it not existing then we'll just continue to do `git rm`
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
|
||||
return c.OSCommand.RunCommand(
|
||||
"git submodule add --force --name %s -- %s %s ",
|
||||
c.OSCommand.Quote(name),
|
||||
c.OSCommand.Quote(url),
|
||||
c.OSCommand.Quote(path),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
|
||||
// the set-url command is only for later git versions so we're doing it manually here
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git submodule sync %s", path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
||||
return c.RunCommand("git submodule init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdate(path string) error {
|
||||
return c.RunCommand("git submodule update --init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
return "git submodule init"
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkUpdateCmdStr() string {
|
||||
return "git submodule update"
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleForceBulkUpdateCmdStr() string {
|
||||
return "git submodule update --force"
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkDeinitCmdStr() string {
|
||||
return "git submodule deinit --all --force"
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
|
||||
for _, submodule := range submodules {
|
||||
if err := c.SubmoduleStash(submodule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.SubmoduleUpdateAll()
|
||||
}
|
||||
76
pkg/commands/sync.go
Normal file
76
pkg/commands/sync.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
followTagsFlag := "--follow-tags"
|
||||
if c.GetConfigValue("push.followTags") == "false" {
|
||||
followTagsFlag = ""
|
||||
}
|
||||
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
}
|
||||
|
||||
setUpstreamArg := ""
|
||||
if upstream != "" {
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
PromptUserForCredential func(string) string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
command := "git fetch"
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
||||
}
|
||||
|
||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
return "\n"
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s", remoteName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
16
pkg/commands/tags.go
Normal file
16
pkg/commands/tags.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
0
pkg/commands/testdata/a_dir/file
vendored
Normal file
0
pkg/commands/testdata/a_dir/file
vendored
Normal file
0
pkg/commands/testdata/a_file
vendored
Normal file
0
pkg/commands/testdata/a_file
vendored
Normal file
@@ -1,25 +1,28 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/shibukawa/configdir"
|
||||
"github.com/spf13/viper"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"github.com/OpenPeeDeeP/xdg"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// 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
|
||||
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 *UserConfig
|
||||
UserConfigDir string
|
||||
UserConfigPath string
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
@@ -31,38 +34,126 @@ type AppConfigurer interface {
|
||||
GetBuildDate() string
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
GetUserConfig() *viper.Viper
|
||||
GetUserConfig() *UserConfig
|
||||
GetUserConfigDir() string
|
||||
GetUserConfigPath() string
|
||||
GetAppState() *AppState
|
||||
WriteToUserConfig(string, string) error
|
||||
SaveAppState() error
|
||||
LoadAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) {
|
||||
userConfig, err := LoadConfig("config", true)
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
||||
configDir, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debuggingFlag = true
|
||||
}
|
||||
|
||||
appState, err := loadAppState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: *debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
AppState: &AppState{},
|
||||
}
|
||||
|
||||
if err := appConfig.LoadAppState(); err != nil {
|
||||
return nil, err
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
UserConfigDir: configDir,
|
||||
UserConfigPath: filepath.Join(configDir, "config.yml"),
|
||||
AppState: appState,
|
||||
IsNewRepo: false,
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
||||
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||
return legacyConfigDirectory
|
||||
}
|
||||
configDirectory := configDirForVendor("")
|
||||
return configDirectory
|
||||
}
|
||||
|
||||
func configDirForVendor(vendor string) string {
|
||||
envConfigDir := os.Getenv("CONFIG_DIR")
|
||||
if envConfigDir != "" {
|
||||
return envConfigDir
|
||||
}
|
||||
configDirs := xdg.New(vendor, "lazygit")
|
||||
return configDirs.ConfigHome()
|
||||
}
|
||||
|
||||
func findOrCreateConfigDir() (string, error) {
|
||||
folder := ConfigDir()
|
||||
err := os.MkdirAll(folder, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return folder, nil
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
|
||||
return loadUserConfig(configDir, GetDefaultConfig())
|
||||
}
|
||||
|
||||
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
||||
fileName := filepath.Join(configDir, "config.yml")
|
||||
|
||||
if _, err := os.Stat(fileName); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "read-only file system") {
|
||||
return base, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(content, base); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return base, 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
|
||||
@@ -95,96 +186,56 @@ func (c *AppConfig) GetBuildSource() string {
|
||||
}
|
||||
|
||||
// GetUserConfig returns the user config
|
||||
func (c *AppConfig) GetUserConfig() *viper.Viper {
|
||||
func (c *AppConfig) GetUserConfig() *UserConfig {
|
||||
return c.UserConfig
|
||||
}
|
||||
|
||||
// GetUserConfig returns the user config
|
||||
func (c *AppConfig) GetUserConfigPath() string {
|
||||
return c.UserConfigPath
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
// LoadConfig gets the user's config
|
||||
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) {
|
||||
v, err := newViper(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withDefaults {
|
||||
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = LoadAndMergeFile(v, filename+".yml"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// LoadDefaults loads in the defaults defined in this file
|
||||
func LoadDefaults(v *viper.Viper, defaults []byte) error {
|
||||
return v.MergeConfig(bytes.NewBuffer(defaults))
|
||||
}
|
||||
|
||||
func prepareConfigFile(filename string) (string, error) {
|
||||
// chucking my name there is not for vanity purposes, the xdg spec (and that
|
||||
// function) requires a vendor name. May as well line up with github
|
||||
configDirs := configdir.New("jesseduffield", "lazygit")
|
||||
folder := configDirs.QueryFolderContainsFile(filename)
|
||||
if folder == nil {
|
||||
// create the file as empty
|
||||
folders := configDirs.QueryFolders(configdir.Global)
|
||||
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
folder = configDirs.QueryFolderContainsFile(filename)
|
||||
}
|
||||
return filepath.Join(folder.Path, filename), nil
|
||||
}
|
||||
|
||||
// LoadAndMergeFile Loads the config/state file, creating
|
||||
// the file as an empty one if it does not exist
|
||||
func LoadAndMergeFile(v *viper.Viper, filename string) error {
|
||||
configPath, err := prepareConfigFile(filename)
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.AddConfigPath(filepath.Dir(configPath))
|
||||
return v.MergeInConfig()
|
||||
c.UserConfig = userConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
func configFilePath(filename string) (string, error) {
|
||||
folder, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
v.Set(key, value)
|
||||
return v.WriteConfig()
|
||||
return filepath.Join(folder, filename), nil
|
||||
}
|
||||
|
||||
// SaveAppState marhsalls the AppState struct and writes it to the disk
|
||||
// ConfigFilename returns the filename of the current config file
|
||||
func (c *AppConfig) ConfigFilename() string {
|
||||
return filepath.Join(c.UserConfigDir, "config.yml")
|
||||
}
|
||||
|
||||
// SaveAppState marshalls 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")
|
||||
filepath, err := configFilePath("state.yml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,65 +243,47 @@ func (c *AppConfig) SaveAppState() error {
|
||||
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
}
|
||||
|
||||
// LoadAppState loads recorded AppState from file
|
||||
func (c *AppConfig) LoadAppState() error {
|
||||
filepath, err := prepareConfigFile("state.yml")
|
||||
// loadAppState loads recorded AppState from file
|
||||
func loadAppState() (*AppState, error) {
|
||||
filepath, err := configFilePath("state.yml")
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
appStateBytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appStateBytes) == 0 {
|
||||
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
|
||||
}
|
||||
return yaml.Unmarshal(appStateBytes, c.AppState)
|
||||
}
|
||||
|
||||
// GetDefaultConfig returns the application default configuration
|
||||
func GetDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`gui:
|
||||
## stuff relating to the UI
|
||||
scrollHeight: 2
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often a update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
`)
|
||||
appStateBytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(appStateBytes) == 0 {
|
||||
return getDefaultAppState(), nil
|
||||
}
|
||||
|
||||
appState := &AppState{}
|
||||
err = yaml.Unmarshal(appStateBytes, appState)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return appState, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
}
|
||||
|
||||
func getDefaultAppState() []byte {
|
||||
return []byte(`
|
||||
lastUpdateCheck: 0
|
||||
recentRepos: []
|
||||
`)
|
||||
func getDefaultAppState() *AppState {
|
||||
return &AppState{
|
||||
LastUpdateCheck: 0,
|
||||
RecentRepos: []string{},
|
||||
StartupPopupVersion: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// // commenting this out until we use it again
|
||||
// func homeDirectory() string {
|
||||
// usr, err := user.Current()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// return usr.HomeDir
|
||||
// }
|
||||
func LogPath() (string, error) {
|
||||
return configFilePath("development.log")
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'open {{filename}}'`)
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +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"'`)
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
|
||||
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package config
|
||||
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'`)
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
OpenCommand: `cmd /c "start "" {{filename}}"`,
|
||||
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
|
||||
}
|
||||
}
|
||||
|
||||
20
pkg/config/dummies.go
Normal file
20
pkg/config/dummies.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
func NewDummyAppConfig() *AppConfig {
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
Commit: "",
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: GetDefaultConfig(),
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user