mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare commits
858
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76ed204795 | ||
|
|
b07c4fc001 | ||
|
|
2304ffc804 | ||
|
|
401610c0ef | ||
|
|
64b2685c2d | ||
|
|
7d4bfb6621 | ||
|
|
a2778f01c6 | ||
|
|
d161afe37f | ||
|
|
3dd96a8010 | ||
|
|
681a9bf20d | ||
|
|
0606b7a43b | ||
|
|
33da56eeb4 | ||
|
|
46b93bba0e | ||
|
|
d2d50aedd0 | ||
|
|
ea5fb6a364 | ||
|
|
a82134f41c | ||
|
|
00b03079d8 | ||
|
|
9592686629 | ||
|
|
114ad52ff6 | ||
|
|
9514284f8e | ||
|
|
ece14844bb | ||
|
|
9910a7c308 | ||
|
|
910a61dc46 | ||
|
|
e5dd4d3110 | ||
|
|
595c7ee73e | ||
|
|
051af6a066 | ||
|
|
e2d7e461a8 | ||
|
|
7c66ca83c1 | ||
|
|
007b406b14 | ||
|
|
2b30085dba | ||
|
|
5c95d23169 | ||
|
|
e156e090cc | ||
|
|
0accb07dcc | ||
|
|
3cac14c76e | ||
|
|
1636931c2b | ||
|
|
6e027f42da | ||
|
|
5149b24ab3 | ||
|
|
c520c5cfc3 | ||
|
|
c88ecdf87c | ||
|
|
d607b366cb | ||
|
|
c8f26aca68 | ||
|
|
30656b5ac6 | ||
|
|
5dacbb6293 | ||
|
|
88d4313970 | ||
|
|
4160fa518c | ||
|
|
fba1a2b5ac | ||
|
|
c6c4346d48 | ||
|
|
ee9ae8f07f | ||
|
|
8d68ab41b6 | ||
|
|
af97bf484c | ||
|
|
a57310df24 | ||
|
|
9d68b287db | ||
|
|
a5c72d056d | ||
|
|
8a86de85c8 | ||
|
|
dd31f8ecea | ||
|
|
5a7b2ab6d0 | ||
|
|
68a9d7fd77 | ||
|
|
19cbafcdfc | ||
|
|
f2c85c5b19 | ||
|
|
0faa41e6f8 | ||
|
|
2e32e55957 | ||
|
|
037cd99138 | ||
|
|
6388885699 | ||
|
|
d3e9bc2185 | ||
|
|
ea4587a3b8 | ||
|
|
2da300f2fb | ||
|
|
7848958326 | ||
|
|
2cba98e3fe | ||
|
|
f8c9ce33c2 | ||
|
|
71753770ad | ||
|
|
820b1e811d | ||
|
|
4a33fede7b | ||
|
|
db12853bbe | ||
|
|
711674f6cd | ||
|
|
fc91ef6a59 | ||
|
|
43251e7275 | ||
|
|
f081358943 | ||
|
|
0c6ab4b43e | ||
|
|
1b2fb34ffd | ||
|
|
0e5a4c7a36 | ||
|
|
47b91f1ef5 | ||
|
|
e2db6a1732 | ||
|
|
509e3efa70 | ||
|
|
8edad826ca | ||
|
|
3ec416047f | ||
|
|
8ed29f2b7d | ||
|
|
9adbef40de | ||
|
|
0cd5257523 | ||
|
|
49da7b482d | ||
|
|
d675117289 | ||
|
|
e63858215e | ||
|
|
3fe4db9316 | ||
|
|
185bbf0c75 | ||
|
|
a8586ba57e | ||
|
|
dad7a70bf8 | ||
|
|
3791f0b2fa | ||
|
|
b8fbe9756e | ||
|
|
ab25600ccb | ||
|
|
d50c58b4c6 | ||
|
|
5645a662de | ||
|
|
a41218551d | ||
|
|
d210107caa | ||
|
|
826128a8e0 | ||
|
|
aec46942a8 | ||
|
|
aa70723e3a | ||
|
|
7db54a948a | ||
|
|
32556480da | ||
|
|
2553015029 | ||
|
|
6c010a788c | ||
|
|
b17c38befd | ||
|
|
79dc1d9052 | ||
|
|
28d2b1432b | ||
|
|
ed98e11fa0 | ||
|
|
07a22e69e7 | ||
|
|
bf3dd79b7a | ||
|
|
21072226d2 | ||
|
|
b09000194a | ||
|
|
3546ab8f21 | ||
|
|
9a13447b97 | ||
|
|
1efb565b22 | ||
|
|
08a445eb9d | ||
|
|
28501fbf77 | ||
|
|
8f1f712841 | ||
|
|
b82b6a2992 | ||
|
|
ddcd6be245 | ||
|
|
711be78811 | ||
|
|
3422b1e218 | ||
|
|
a3fdf91714 | ||
|
|
368f9c8cb3 | ||
|
|
e18b4a4cc3 | ||
|
|
46718e25ca | ||
|
|
d675eb6507 | ||
|
|
dc4e88f8a4 | ||
|
|
120dd1530a | ||
|
|
860a8d102b | ||
|
|
a304fed68c | ||
|
|
227b0b781c | ||
|
|
740474c10c | ||
|
|
a0d179b6dc | ||
|
|
c53c5e47ef | ||
|
|
188773511e | ||
|
|
62c5c32fbb | ||
|
|
981ba4c66c | ||
|
|
7f5465a27b | ||
|
|
275ed12486 | ||
|
|
6a340ec840 | ||
|
|
3535cd0f94 | ||
|
|
b1a56249f5 | ||
|
|
82c54ed3d2 | ||
|
|
bd62b519de | ||
|
|
04e0a9bb45 | ||
|
|
2b4ac986a2 | ||
|
|
caedf57484 | ||
|
|
046b0d9daa | ||
|
|
e3c5e07b20 | ||
|
|
e4e16fa38e | ||
|
|
b7e029adc7 | ||
|
|
08d679c3a8 | ||
|
|
2947b56134 | ||
|
|
659d668e16 | ||
|
|
7bbcec965b | ||
|
|
24de156592 | ||
|
|
8d3cce4a49 | ||
|
|
95757ceb3b | ||
|
|
2087a02f01 | ||
|
|
6ffe98abac | ||
|
|
046cb942c2 | ||
|
|
fc2f8b7b20 | ||
|
|
298dae23e8 | ||
|
|
a02b54e1b7 | ||
|
|
0af4e5a843 | ||
|
|
e0503b9922 | ||
|
|
ef239c04fb | ||
|
|
d508badd62 | ||
|
|
e7d0116312 | ||
|
|
85fdb700ba | ||
|
|
7513d77567 | ||
|
|
c757063264 | ||
|
|
b24955063c | ||
|
|
605bc026a1 | ||
|
|
c6930e0538 | ||
|
|
3a59aba46d | ||
|
|
213ae8dd07 | ||
|
|
4780953cef | ||
|
|
11bc8b87fa | ||
|
|
3bfbc9d255 | ||
|
|
8121a0cc74 | ||
|
|
4adca84d68 | ||
|
|
b7c61aa883 | ||
|
|
40f6767cfc | ||
|
|
776d8f4d2e | ||
|
|
4b67a45a16 | ||
|
|
a82d952f48 | ||
|
|
549ce09f71 | ||
|
|
cef804f27a | ||
|
|
60f902f026 | ||
|
|
7ce3165afa | ||
|
|
c28e25524a | ||
|
|
73c7dc9c5d | ||
|
|
b542579db3 | ||
|
|
e6274af015 | ||
|
|
4f57bf22c9 | ||
|
|
8dbd7d44ff | ||
|
|
4b4dccfd7d | ||
|
|
81ea3107ed | ||
|
|
5c8bc790ff | ||
|
|
b5d612e6d6 | ||
|
|
dea279920c | ||
|
|
5834440767 | ||
|
|
55fb3ef4e6 | ||
|
|
f314cb3763 | ||
|
|
516e963392 | ||
|
|
804a134aa5 | ||
|
|
82fc6fb111 | ||
|
|
0bda93d4c3 | ||
|
|
6735bf4c89 | ||
|
|
4bd1322941 | ||
|
|
45cf993982 | ||
|
|
e4659145e8 | ||
|
|
bf6e9a1bd3 | ||
|
|
a68cd6af9c | ||
|
|
4ca012dbfb | ||
|
|
6bd1c1d068 | ||
|
|
5d692e8961 | ||
|
|
9cc33c479b | ||
|
|
c79e360584 | ||
|
|
f76cc27956 | ||
|
|
5a50bfd179 | ||
|
|
c36333af3d | ||
|
|
4d78d76a44 | ||
|
|
d08a9849c3 | ||
|
|
3791b5057a | ||
|
|
dfe5c805c4 | ||
|
|
e94ff63bc5 | ||
|
|
723504a290 | ||
|
|
40a29fd622 | ||
|
|
648748781d | ||
|
|
63e8b8c01c | ||
|
|
ed47529604 | ||
|
|
ff5d0155db | ||
|
|
48acf3818f | ||
|
|
de3e4838ad | ||
|
|
6af8f278d0 | ||
|
|
e3c6887e5d | ||
|
|
c34e0deca7 | ||
|
|
75b2c50a55 | ||
|
|
af7c71d1ee | ||
|
|
161bb684fa | ||
|
|
a624e0457f | ||
|
|
f2aa7e7e28 | ||
|
|
75aa339b4e | ||
|
|
45d45d2397 | ||
|
|
f7e8b2dd71 | ||
|
|
8b5d59c238 | ||
|
|
f82f4f6dbc | ||
|
|
ff3c5d331e | ||
|
|
adef3bd4ca | ||
|
|
33f9f81c8c | ||
|
|
f6fafc65ee | ||
|
|
9c645088bf | ||
|
|
dd1bf629b8 | ||
|
|
c80a94aa7a | ||
|
|
6c0b805137 | ||
|
|
8a69d994c0 | ||
|
|
037e957282 | ||
|
|
db011d8e34 | ||
|
|
752526c880 | ||
|
|
c63fed2074 | ||
|
|
1b52a0d83f | ||
|
|
909a3b6791 | ||
|
|
558ceb64c7 | ||
|
|
1034962c7e | ||
|
|
eabe7f462a | ||
|
|
22c10479d5 | ||
|
|
f0572238cb | ||
|
|
78f3a7a478 | ||
|
|
90772e1eaa | ||
|
|
526c9dea9b | ||
|
|
c244c8f231 | ||
|
|
bfde06d049 | ||
|
|
6b8abb7887 | ||
|
|
2b6a109e38 | ||
|
|
71a30155dc | ||
|
|
6ee20840b2 | ||
|
|
56424eb1aa | ||
|
|
38c7030b0f | ||
|
|
31b8524fe6 | ||
|
|
0ae34aeeb7 | ||
|
|
e1c376ef54 | ||
|
|
c13f550d63 | ||
|
|
06351c1adf | ||
|
|
0b55eaca1d | ||
|
|
ee8c31880c | ||
|
|
daf8176dd7 | ||
|
|
76109a4c44 | ||
|
|
082d342bf8 | ||
|
|
39c56553b3 | ||
|
|
c5cd217a65 | ||
|
|
9e2a3a87dd | ||
|
|
ac580ae6a0 | ||
|
|
9e1e20fef2 | ||
|
|
4b49bd406f | ||
|
|
7351907474 | ||
|
|
a349e886ce | ||
|
|
dd61c49a15 | ||
|
|
1c3db24e44 | ||
|
|
15962e489d | ||
|
|
ac9515d8c7 | ||
|
|
72a92d748f | ||
|
|
36c2b00336 | ||
|
|
67b08ac239 | ||
|
|
67b8ef449c | ||
|
|
bb856ad7c6 | ||
|
|
b54b8ae746 | ||
|
|
65bd0ab431 | ||
|
|
93b9e1bd19 | ||
|
|
a51f64814c | ||
|
|
b5e325b0a4 | ||
|
|
88c76868ba | ||
|
|
e471567b5d | ||
|
|
13ee0f0a5d | ||
|
|
76a1b501f2 | ||
|
|
7201a91b69 | ||
|
|
31d796ee75 | ||
|
|
0ac869a415 | ||
|
|
9da9143aed | ||
|
|
b0383ba73a | ||
|
|
a3096e720c | ||
|
|
c599aaed51 | ||
|
|
f999bbce7c | ||
|
|
bff076c70a | ||
|
|
979c3d6278 | ||
|
|
1fc8823825 | ||
|
|
eeb9c02836 | ||
|
|
4d39668743 | ||
|
|
627cc285f7 | ||
|
|
bee338f93b | ||
|
|
01bf7f21e6 | ||
|
|
c517d1e0a2 | ||
|
|
8cad8cda8f | ||
|
|
6b81e6adca | ||
|
|
b499eba1a8 | ||
|
|
97daec7228 | ||
|
|
ff2a799200 | ||
|
|
31fcec16d9 | ||
|
|
0d395e4051 | ||
|
|
09178a1276 | ||
|
|
b66aa42ee5 | ||
|
|
3cfdae4116 | ||
|
|
d7956c481d | ||
|
|
7a3291a1f7 | ||
|
|
08c2b46d04 | ||
|
|
1c48842277 | ||
|
|
010f6d7f6e | ||
|
|
39c20bc634 | ||
|
|
823d95a8c6 | ||
|
|
225cd3cc60 | ||
|
|
91d62da577 | ||
|
|
9f71ed6c41 | ||
|
|
984eb95cb7 | ||
|
|
1da762c295 | ||
|
|
e357c00d4d | ||
|
|
1be6c522d8 | ||
|
|
8af59c3e6e | ||
|
|
c713d19383 | ||
|
|
4f7324bad0 | ||
|
|
aed3d56840 | ||
|
|
3745ba506b | ||
|
|
5e2254395a | ||
|
|
b243f30f48 | ||
|
|
e57843d947 | ||
|
|
5bb6198219 | ||
|
|
bbaeab68e1 | ||
|
|
946c1dff99 | ||
|
|
d838965a41 | ||
|
|
469938ee9b | ||
|
|
f7af2b991d | ||
|
|
75c3ab8a4a | ||
|
|
7bd0c779c7 | ||
|
|
16802a048e | ||
|
|
c0e805718d | ||
|
|
35c5f940a4 | ||
|
|
77093451d4 | ||
|
|
368d6437b8 | ||
|
|
01f0efb997 | ||
|
|
d0851113d1 | ||
|
|
df58c75ca4 | ||
|
|
d8c7d47067 | ||
|
|
f79a8c281f | ||
|
|
996a1e469f | ||
|
|
9d5d61260a | ||
|
|
18db5eafd4 | ||
|
|
2183c157d4 | ||
|
|
5dec080719 | ||
|
|
fc38e3b54d | ||
|
|
93d845cb01 | ||
|
|
67fb28e2b8 | ||
|
|
679b0456f3 | ||
|
|
f7f24dbfc1 | ||
|
|
b942df02a1 | ||
|
|
f12a2edefa | ||
|
|
7e54b5641f | ||
|
|
89a09be6a0 | ||
|
|
069af50f50 | ||
|
|
c6929c36ae | ||
|
|
e8f4508cba | ||
|
|
b8d33b8f7b | ||
|
|
946b8b5670 | ||
|
|
d479a41cad | ||
|
|
7149cfeb11 | ||
|
|
265cdde7bc | ||
|
|
e87fc4a229 | ||
|
|
b45c5d8491 | ||
|
|
f6f82091bc | ||
|
|
48df9b7f4e | ||
|
|
fd86d29400 | ||
|
|
a11e91e651 | ||
|
|
6127e487dd | ||
|
|
1d89a5daa1 | ||
|
|
f4ccb68464 | ||
|
|
b90af5461a | ||
|
|
acbcf9933d | ||
|
|
21f8857d36 | ||
|
|
965f7bfcb2 | ||
|
|
c769a78db5 | ||
|
|
1cf24a02d3 | ||
|
|
657b1e897f | ||
|
|
00b922604a | ||
|
|
bc7873144e | ||
|
|
1bb138c79c | ||
|
|
c8fc1c3f5a | ||
|
|
c5ea80fa67 | ||
|
|
d98130c3ef | ||
|
|
7c5f33980f | ||
|
|
cceff63823 | ||
|
|
5c42e1a5dc | ||
|
|
6c3671f807 | ||
|
|
89ba3a38b4 | ||
|
|
6f709456fe | ||
|
|
277ca706eb | ||
|
|
8a1c763942 | ||
|
|
31bdd27e88 | ||
|
|
e00f248cf7 | ||
|
|
cd9111837e | ||
|
|
41222f07ed | ||
|
|
ae780fdb81 | ||
|
|
a05bdc3ee4 | ||
|
|
1da0427e3a | ||
|
|
af5b3be286 | ||
|
|
81281a49b2 | ||
|
|
ff8823093c | ||
|
|
0300bfdec2 | ||
|
|
f770a6246b | ||
|
|
5e9a897348 | ||
|
|
f2d0f362d4 | ||
|
|
ae07cf5506 | ||
|
|
f3fa9ec2d1 | ||
|
|
e661916ba6 | ||
|
|
0a8731eecf | ||
|
|
14a974742f | ||
|
|
534703a023 | ||
|
|
9fef4447b6 | ||
|
|
7aa843c75a | ||
|
|
a27092a7ad | ||
|
|
a3450dfdfc | ||
|
|
b4e9806352 | ||
|
|
f495945b87 | ||
|
|
47de61b57c | ||
|
|
06c878c051 | ||
|
|
ed93e0a2b0 | ||
|
|
c5050ecabd | ||
|
|
78b495f50a | ||
|
|
53e06b71ae | ||
|
|
b166b8f776 | ||
|
|
c5c9f5bb94 | ||
|
|
09e80e5f2a | ||
|
|
be30cbb375 | ||
|
|
b64f55518b | ||
|
|
926ed7b9b2 | ||
|
|
8052ac4fd6 | ||
|
|
c976839a63 | ||
|
|
ac127f017e | ||
|
|
17140e1d8d | ||
|
|
cd418ec929 | ||
|
|
8c89069965 | ||
|
|
09db4c4397 | ||
|
|
96310288ee | ||
|
|
c841ba8237 | ||
|
|
9a6f21ce42 | ||
|
|
fa0414777f | ||
|
|
5d2584a188 | ||
|
|
fb15a2f4f8 | ||
|
|
ef62a35d79 | ||
|
|
05425cfba0 | ||
|
|
b623ecf898 | ||
|
|
aedfce2845 | ||
|
|
c19f52255c | ||
|
|
fa97b0c76e | ||
|
|
588850b090 | ||
|
|
13639ac924 | ||
|
|
5c11b1ecb7 | ||
|
|
7c7f7bf9b9 | ||
|
|
57a1817deb | ||
|
|
b6c73b3620 | ||
|
|
7bdba1abe4 | ||
|
|
c77df59b9b | ||
|
|
f910b42c9c | ||
|
|
dde70486a1 | ||
|
|
186b7197e4 | ||
|
|
6ec88ce8ba | ||
|
|
e3c6738535 | ||
|
|
bc4ace8357 | ||
|
|
b40190bd94 | ||
|
|
abbd598992 | ||
|
|
5679efe174 | ||
|
|
6bf28d325f | ||
|
|
43b5a80738 | ||
|
|
b13cfdfea0 | ||
|
|
b647241521 | ||
|
|
1e85bc3d62 | ||
|
|
3a1921cab0 | ||
|
|
6386a03805 | ||
|
|
d69b2fef9a | ||
|
|
50b0d85cd3 | ||
|
|
bfcbf228bf | ||
|
|
87e0f6b92d | ||
|
|
d0499286e2 | ||
|
|
0af63daf18 | ||
|
|
8b894d7bf5 | ||
|
|
f7449ed53a | ||
|
|
ff25016a6a | ||
|
|
65d6d7fb2d | ||
|
|
1a1f042f49 | ||
|
|
5b3f684afb | ||
|
|
618b845f5e | ||
|
|
f96246c038 | ||
|
|
773eb0310f | ||
|
|
03ce22f3c9 | ||
|
|
b3d086bdc1 | ||
|
|
cf048e4807 | ||
|
|
a6ebc5869e | ||
|
|
ffc2a6805a | ||
|
|
d24feb14e5 | ||
|
|
245563bc99 | ||
|
|
37997dcbcd | ||
|
|
924a152ae9 | ||
|
|
2b8e412ed3 | ||
|
|
d1a8b05401 | ||
|
|
de22238589 | ||
|
|
52a2e4c1dc | ||
|
|
e953659ebf | ||
|
|
b33ec5a050 | ||
|
|
fbfec75a99 | ||
|
|
3e73dacce3 | ||
|
|
684d1e955e | ||
|
|
f67824b349 | ||
|
|
c53752a5f9 | ||
|
|
dc163bfc4d | ||
|
|
3c436b3457 | ||
|
|
526d02de1c | ||
|
|
a905a28e41 | ||
|
|
5964472ec1 | ||
|
|
c77c02c879 | ||
|
|
d26350502c | ||
|
|
2ddd3ddfc0 | ||
|
|
df3cd941d7 | ||
|
|
ea28529cbb | ||
|
|
db592e1dc6 | ||
|
|
863a65cf94 | ||
|
|
5eeaebde98 | ||
|
|
e059641f3f | ||
|
|
fbac05fff7 | ||
|
|
99e05ee138 | ||
|
|
7144691ae5 | ||
|
|
fb170b55a2 | ||
|
|
903e65ae8d | ||
|
|
e56b05cbac | ||
|
|
9e91aa8b43 | ||
|
|
7977c2deb5 | ||
|
|
d7fb441a3c | ||
|
|
f03e1e4e42 | ||
|
|
91f83c6be7 | ||
|
|
fd581ef70c | ||
|
|
f98b2edae5 | ||
|
|
97ced9e14f | ||
|
|
ea09686770 | ||
|
|
c769e5828c | ||
|
|
33410213f1 | ||
|
|
af57dbd225 | ||
|
|
66a253916e | ||
|
|
188890fc49 | ||
|
|
3e15315339 | ||
|
|
4fa8586191 | ||
|
|
3a295f34df | ||
|
|
1ac3ae1ad1 | ||
|
|
34404162e6 | ||
|
|
e6e95343af | ||
|
|
52316e628e | ||
|
|
e8b97c9fe2 | ||
|
|
ab03cf8bcf | ||
|
|
cc01c0de6d | ||
|
|
4f471f352f | ||
|
|
a63d5c5644 | ||
|
|
a47e72892a | ||
|
|
c4e71356bb | ||
|
|
b0f7cf092f | ||
|
|
808b35d8f2 | ||
|
|
4aa9147dfa | ||
|
|
14ec0cd92e | ||
|
|
36c6462a53 | ||
|
|
2ec0b671e6 | ||
|
|
3103398e31 | ||
|
|
e78e829e3a | ||
|
|
eceb3a5aa6 | ||
|
|
8a9eefa4d2 | ||
|
|
11316b7a48 | ||
|
|
8f3ccd07db | ||
|
|
79334b84f0 | ||
|
|
39e84e13f4 | ||
|
|
0c3eab4059 | ||
|
|
05089b9a9a | ||
|
|
d90fedfbf8 | ||
|
|
6af0afdb1c | ||
|
|
6cb5e628b3 | ||
|
|
a4239c7a37 | ||
|
|
fc0b14edef | ||
|
|
6b9f43c73b | ||
|
|
1358662a21 | ||
|
|
ffe5e16688 | ||
|
|
575afa1377 | ||
|
|
d22f6d73ff | ||
|
|
867975ad31 | ||
|
|
24a07ae85a | ||
|
|
c370a5e728 | ||
|
|
dba0edb998 | ||
|
|
69040f094d | ||
|
|
8c46a0110d | ||
|
|
a30d924afe | ||
|
|
f4c188fa5b | ||
|
|
7b4b42abd6 | ||
|
|
2f76ef58f1 | ||
|
|
e436922eb6 | ||
|
|
056cc14821 | ||
|
|
c686c7f9ed | ||
|
|
b27a2ce39a | ||
|
|
e3f21f0588 | ||
|
|
3375cc1b3d | ||
|
|
ed98b60078 | ||
|
|
e76fa5a6cb | ||
|
|
a77aa4d75a | ||
|
|
68a2a4c6b5 | ||
|
|
8858f03606 | ||
|
|
5670c0a301 | ||
|
|
c953871ec7 | ||
|
|
b0c19b291f | ||
|
|
8a022ddf0e | ||
|
|
e16f1ba84f | ||
|
|
19df238b77 | ||
|
|
092363a986 | ||
|
|
23d39c79b2 | ||
|
|
0141bbde0e | ||
|
|
4c7d363959 | ||
|
|
41f86f6535 | ||
|
|
90feb4bae6 | ||
|
|
1d40bd1707 | ||
|
|
17df42e517 | ||
|
|
fd66499c8f | ||
|
|
e001183768 | ||
|
|
268d8f99b8 | ||
|
|
212e19f598 | ||
|
|
1c82924307 | ||
|
|
c8066c54b5 | ||
|
|
438038a4f1 | ||
|
|
f23547580a | ||
|
|
3d79c6a3d3 | ||
|
|
bc8050d8ac | ||
|
|
b8900baf1a | ||
|
|
dcbebef897 | ||
|
|
7e9dffe1b9 | ||
|
|
c81333fefe | ||
|
|
5639af9918 | ||
|
|
ee348751a0 | ||
|
|
a92f0f7c89 | ||
|
|
6dca3e1766 | ||
|
|
850a82784a | ||
|
|
74acb3e86a | ||
|
|
9351af3829 | ||
|
|
7af7af27c6 | ||
|
|
db9373662a | ||
|
|
e189546acb | ||
|
|
c7733aa5e5 | ||
|
|
a0fd47348b | ||
|
|
6feb301c2a | ||
|
|
e66b162726 | ||
|
|
4f8816ebf2 | ||
|
|
50cf7ac5bc | ||
|
|
088445b7be | ||
|
|
c7fd218308 | ||
|
|
7ddb80a13e | ||
|
|
fecf2ab810 | ||
|
|
3232f46a8b | ||
|
|
7b757d1cfe | ||
|
|
843488bff4 | ||
|
|
a9d4ff2aee | ||
|
|
5d5471c017 | ||
|
|
8cdfc6758f | ||
|
|
010f430d1f | ||
|
|
79620fc6cf | ||
|
|
47f84b6aea | ||
|
|
de6d278e57 | ||
|
|
46b08e7d70 | ||
|
|
eb9fbb0a33 | ||
|
|
448ff80d7d | ||
|
|
74f9b8a3b4 | ||
|
|
25c4aa532e | ||
|
|
e7aacafc2e | ||
|
|
d78d694959 | ||
|
|
a67a08eeac | ||
|
|
f6d6b5dec0 | ||
|
|
ce98279896 | ||
|
|
e0e4138396 | ||
|
|
014daf7bc0 | ||
|
|
67d6b69115 | ||
|
|
956372cf8a | ||
|
|
290e865584 | ||
|
|
6248091e9c | ||
|
|
3ada4dde12 | ||
|
|
ef82f39431 | ||
|
|
21a4522a51 | ||
|
|
154bd975a6 | ||
|
|
fbe54512a8 | ||
|
|
7fd0e55b7f | ||
|
|
13b04e9e8c | ||
|
|
8a1937787d | ||
|
|
a94c703afb | ||
|
|
6d7a7afbbc | ||
|
|
6abcfd5cba | ||
|
|
a95d3e26b3 | ||
|
|
af45692e24 | ||
|
|
fed2aaf37f | ||
|
|
b2ae651686 | ||
|
|
e875d6b448 | ||
|
|
53979f7cec | ||
|
|
9c0d860980 | ||
|
|
4aea005f26 | ||
|
|
39e9266089 | ||
|
|
f999c90a7b | ||
|
|
502723421b | ||
|
|
5173d7f5e1 | ||
|
|
349a7d2453 | ||
|
|
f3837000dd | ||
|
|
d1b093e703 | ||
|
|
c66a2c3465 | ||
|
|
ba7d639940 | ||
|
|
cad84c9e74 | ||
|
|
5e475355bf | ||
|
|
304d74370e | ||
|
|
2bdefe2049 | ||
|
|
faed509bfd | ||
|
|
b8d9443999 | ||
|
|
de84b6c4b9 | ||
|
|
610eddfe05 | ||
|
|
1ef6f4c0e1 | ||
|
|
ae798157d2 | ||
|
|
a45b22e12f | ||
|
|
ba96baee32 | ||
|
|
d890238c7b | ||
|
|
46ae55f91e | ||
|
|
0ff5b74d80 | ||
|
|
225c563c63 | ||
|
|
77881a9c7d | ||
|
|
c7f9d5801b | ||
|
|
77622e5638 | ||
|
|
6422b399e7 | ||
|
|
3016469708 | ||
|
|
8b371ada73 | ||
|
|
fc49068a6f | ||
|
|
95297f72a3 | ||
|
|
121872ac27 | ||
|
|
d072b0c75e | ||
|
|
70a46028e1 | ||
|
|
e4e04cfa8f | ||
|
|
2e7b935bfb | ||
|
|
71a9389ca4 | ||
|
|
b77f3160f1 | ||
|
|
92bde6dda4 | ||
|
|
d73a236d7c | ||
|
|
fcf20f3b93 | ||
|
|
9578329cd9 | ||
|
|
4c5fa83566 | ||
|
|
755ae0ef84 | ||
|
|
7410acd1aa | ||
|
|
4db22aec0e | ||
|
|
a91d977f89 | ||
|
|
445a625b56 | ||
|
|
cd2c01d1cf | ||
|
|
17316ca061 | ||
|
|
54fb73080a | ||
|
|
524bf83a4a | ||
|
|
7fcd1d2a6e | ||
|
|
fdf6a9cc2b | ||
|
|
57f86b8f90 | ||
|
|
6160d85d4f | ||
|
|
64224e7caa | ||
|
|
d56bb0b8ef | ||
|
|
d238d8952b | ||
|
|
86d5654d20 | ||
|
|
6dfef08efc | ||
|
|
c1c6e2fac2 | ||
|
|
0b4f9f8c76 | ||
|
|
a658cd4076 | ||
|
|
69718fb557 | ||
|
|
b9b2f58bc8 | ||
|
|
bdb0b9ae6e | ||
|
|
2866827ca8 | ||
|
|
932b0b593e | ||
|
|
3ee2ad511e | ||
|
|
debc58b6c5 | ||
|
|
fab2e14b55 | ||
|
|
81f80ce968 | ||
|
|
95426c5e46 | ||
|
|
4ffc9a5395 | ||
|
|
86ac309e08 | ||
|
|
9987e65c35 | ||
|
|
69f4292fe3 | ||
|
|
682be18507 | ||
|
|
2ca2acaca5 | ||
|
|
5f4c29d7b5 | ||
|
|
44de380c2b | ||
|
|
37bdbd9a21 | ||
|
|
767ef31661 | ||
|
|
bb64fe4309 | ||
|
|
c81c046615 | ||
|
|
a905165046 | ||
|
|
7077ea428f | ||
|
|
f2880ecb46 | ||
|
|
966733240c | ||
|
|
25ddac0d8f | ||
|
|
7c09ce3871 | ||
|
|
e6e4513f45 | ||
|
|
9c6239df3d | ||
|
|
c26650258d | ||
|
|
c943cdd8d2 | ||
|
|
ab5a8091f5 | ||
|
|
95678b1b43 | ||
|
|
f947214ce0 | ||
|
|
8c65f63e76 | ||
|
|
085904d209 | ||
|
|
cc377b5c49 | ||
|
|
1f97ddd9b1 | ||
|
|
5cef0f2921 | ||
|
|
61e14c46b2 | ||
|
|
1f482e585e | ||
|
|
41b54d742f | ||
|
|
83dfc3b28b | ||
|
|
017d44ff17 | ||
|
|
168fbe0a6c | ||
|
|
e1f41b653c |
@@ -0,0 +1,13 @@
|
||||
# adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/Dockerfile
|
||||
|
||||
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
|
||||
ARG VARIANT=1-bullseye
|
||||
FROM golang:${VARIANT}
|
||||
|
||||
RUN go install mvdan.cc/gofumpt@latest
|
||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.50.0
|
||||
RUN golangci-lint --version
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
@@ -0,0 +1,69 @@
|
||||
// adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/devcontainer.json
|
||||
{
|
||||
"build": {
|
||||
"dockerfile": "./Dockerfile",
|
||||
"context": "."
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:1": {
|
||||
"installZsh": "true",
|
||||
"username": "vscode",
|
||||
"uid": "1000",
|
||||
"gid": "1000",
|
||||
"upgradePackages": "true"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "none"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git:1": {
|
||||
"version": "latest",
|
||||
"ppa": "false"
|
||||
}
|
||||
},
|
||||
"overrideFeatureInstallOrder": [
|
||||
"ghcr.io/devcontainers/features/common-utils"
|
||||
],
|
||||
// not sure if we actually need these
|
||||
"runArgs": [
|
||||
"--cap-add=SYS_PTRACE",
|
||||
"--security-opt",
|
||||
"seccomp=unconfined"
|
||||
],
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"go.toolsManagement.checkForUpdates": "local",
|
||||
"go.useLanguageServer": true,
|
||||
"go.gopath": "/go",
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true,
|
||||
"usePlaceholders": false // add parameter placeholders when completing a function
|
||||
},
|
||||
"files.eol": "\n"
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"golang.Go"
|
||||
]
|
||||
}
|
||||
},
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "go version",
|
||||
|
||||
// See https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/ for the safe.directory part
|
||||
// The defaultBranch part is required for our deprecated integration tests.
|
||||
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && git config --global init.defaultBranch master",
|
||||
|
||||
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -4,7 +4,6 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
@@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -23,10 +23,15 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows]
|
||||
- Lazygit Version [e.g. v0.1.45]
|
||||
- The last commit id if you built project from sources (run : ```git rev-parse HEAD```)
|
||||
**Version info:**
|
||||
_Run `lazygit --version` and paste the result here_
|
||||
_Run `git --version` and paste the result here_
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
**Note:** please try updating to the latest version or [manually building](https://github.com/jesseduffield/lazygit/#manual) the latest `master` to see if the issue still occurs.
|
||||
|
||||
<!--
|
||||
If you want to try and debug this issue yourself, you can run `lazygit --debug` in one terminal panel and `lazygit --logs` in another to view the logs.
|
||||
-->
|
||||
|
||||
@@ -4,7 +4,6 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
@@ -18,3 +17,13 @@ A clear and concise description of any alternative solutions or features you've
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
<!--
|
||||
You may be able to add your desired feature with a custom command. Check out the examples here: https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium
|
||||
|
||||
If a custom command does what you want but you still want to see the feature built-in to lazygit, feel free to paste the custom command into the issue to help us better understand the functionality you want.
|
||||
|
||||
We also encourage you to put up a PR yourself! Who cares if you've never written Go before, neither did any of the existing contributors before their first lazygit PR! Check out the PR tutorial here: https://www.youtube.com/watch?v=kNavnhzZHtk&ab_channel=JesseDuffield
|
||||
|
||||
Also check out the contributing guide here: https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
- **PR Description**
|
||||
|
||||
- **Please check if the PR fulfills these requirements**
|
||||
|
||||
* [ ] Cheatsheets are up-to-date (run `go run scripts/cheatsheet/main.go generate`)
|
||||
* [ ] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
|
||||
* [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide)
|
||||
* [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
|
||||
* [ ] Docs (specifically `docs/Config.md`) have been updated if necessary
|
||||
* [ ] You've read through your own file changes for silly mistakes etc
|
||||
@@ -1,28 +0,0 @@
|
||||
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
|
||||
+21
-14
@@ -48,12 +48,7 @@ jobs:
|
||||
go test ./... -short
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallelism: [5]
|
||||
index: [0,1,2,3,4]
|
||||
name: "Integration Tests (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
name: "Integration Tests"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@@ -73,8 +68,9 @@ jobs:
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
# LONG_WAIT_BEFORE_FAIL means that for a given test assertion, we'll wait longer before failing
|
||||
run: |
|
||||
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/gui/gui_test.go
|
||||
LONG_WAIT_BEFORE_FAIL=true go test pkg/integration/clients/*.go
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -105,7 +101,13 @@ jobs:
|
||||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
check-cheatsheet:
|
||||
- name: Build integration test binary
|
||||
run: |
|
||||
GOOS=linux go build cmd/integration_test/main.go
|
||||
- name: Build integration test injector
|
||||
run: |
|
||||
GOOS=linux go build pkg/integration/clients/injector/main.go
|
||||
check-codebase:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
@@ -129,6 +131,17 @@ jobs:
|
||||
- name: Check Cheatsheet
|
||||
run: |
|
||||
go run scripts/cheatsheet/main.go check
|
||||
- name: Check Vendor Directory
|
||||
# ensure our vendor directory matches up with our go modules
|
||||
run: |
|
||||
go mod vendor && git diff --exit-code || (echo "Unexpected change to vendor directory. Run 'go mod vendor' locally and commit the changes" && exit 1)
|
||||
- name: Check Integration Test List
|
||||
# ensure our integration test list is up to date
|
||||
run: |
|
||||
go generate pkg/integration/tests/tests.go && git diff --exit-code || (echo "Integration test list not up to date. Run 'go generate pkg/integration/tests/tests.go' locally and commit the changes" && exit 1)
|
||||
shell: bash # needed so that we get "-o pipefail"
|
||||
- name: Check Filenames
|
||||
run: scripts/check_filenames.sh
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -153,12 +166,6 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v3.1.0
|
||||
with:
|
||||
version: latest
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
- name: errors
|
||||
run: golangci-lint run
|
||||
if: ${{ failure() }}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Build PR Image
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
jobs:
|
||||
build-application:
|
||||
name: Build and Push `lazygit`
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }}
|
||||
outputs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Generate UUID image name
|
||||
id: uuid
|
||||
run: echo "UUID_APP_TAG=$(uuidgen)" >> $GITHUB_ENV
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: registry.uffizzi.com/${{ env.UUID_APP_TAG }}
|
||||
tags: type=raw,value=60d
|
||||
- name: Build and Push Image to registry.uffizzi.com ephemeral registry
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
push: true
|
||||
context: ./
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: ./uffizzi/DockerfileTtyd
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
render-compose-file:
|
||||
name: Render Docker Compose File
|
||||
# Pass output of this workflow to another triggered by `workflow_run` event.
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-application
|
||||
outputs:
|
||||
compose-file-cache-key: ${{ steps.hash.outputs.hash }}
|
||||
steps:
|
||||
- name: Checkout git repo
|
||||
uses: actions/checkout@v3
|
||||
- name: Render Compose File
|
||||
run: |
|
||||
APP_IMAGE=$(echo ${{ needs.build-application.outputs.tags }})
|
||||
export APP_IMAGE
|
||||
# Render simple template from environment variables.
|
||||
envsubst < ./uffizzi/docker-compose.uffizzi.yml > docker-compose.rendered.yml
|
||||
cat docker-compose.rendered.yml
|
||||
- name: Upload Rendered Compose File as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: docker-compose.rendered.yml
|
||||
retention-days: 2
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
|
||||
delete-preview:
|
||||
name: Call for Preview Deletion
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.action == 'closed' }}
|
||||
steps:
|
||||
# If this PR is closing, we will not render a compose file nor pass it to the next workflow.
|
||||
- name: Upload PR Event as Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: preview-spec
|
||||
path: ${{ github.event_path }}
|
||||
retention-days: 2
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Deploy Uffizzi Preview
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Build PR Image"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
cache-compose-file:
|
||||
name: Cache Compose File
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
outputs:
|
||||
compose-file-cache-key: ${{ env.COMPOSE_FILE_HASH }}
|
||||
pr-number: ${{ env.PR_NUMBER }}
|
||||
steps:
|
||||
- name: 'Download artifacts'
|
||||
# Fetch output (zip archive) from the workflow run that triggered this workflow.
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "preview-spec"
|
||||
})[0];
|
||||
if (matchArtifact === undefined) {
|
||||
throw TypeError('Build Artifact not found!');
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/preview-spec.zip`, Buffer.from(download.data));
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip preview-spec.zip
|
||||
|
||||
- name: Read Event into ENV
|
||||
run: |
|
||||
echo 'EVENT_JSON<<EOF' >> $GITHUB_ENV
|
||||
cat event.json >> $GITHUB_ENV
|
||||
echo -e '\nEOF' >> $GITHUB_ENV
|
||||
- name: Hash Rendered Compose File
|
||||
id: hash
|
||||
# If the previous workflow was triggered by a PR close event, we will not have a compose file artifact.
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
run: echo "COMPOSE_FILE_HASH=$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Rendered Compose File
|
||||
if: ${{ fromJSON(env.EVENT_JSON).action != 'closed' }}
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: docker-compose.rendered.yml
|
||||
key: ${{ env.COMPOSE_FILE_HASH }}
|
||||
|
||||
- name: Read PR Number From Event Object
|
||||
id: pr
|
||||
run: echo "PR_NUMBER=${{ fromJSON(env.EVENT_JSON).number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: DEBUG - Print Job Outputs
|
||||
if: ${{ runner.debug }}
|
||||
run: |
|
||||
echo "PR number: ${{ env.PR_NUMBER }}"
|
||||
echo "Compose file hash: ${{ env.COMPOSE_FILE_HASH }}"
|
||||
cat event.json
|
||||
deploy-uffizzi-preview:
|
||||
name: Use Remote Workflow to Preview on Uffizzi
|
||||
needs:
|
||||
- cache-compose-file
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml@v2
|
||||
with:
|
||||
# If this workflow was triggered by a PR close event, cache-key will be an empty string
|
||||
# and this reusable workflow will delete the preview deployment.
|
||||
compose-file-cache-key: ${{ needs.cache-compose-file.outputs.compose-file-cache-key }}
|
||||
compose-file-cache-path: docker-compose.rendered.yml
|
||||
server: https://app.uffizzi.com
|
||||
pr-number: ${{ needs.cache-compose-file.outputs.pr-number }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
+4
-6
@@ -27,17 +27,15 @@ lazygit.exe
|
||||
!.circleci/
|
||||
!.github/
|
||||
!.vscode/
|
||||
!.devcontainer/
|
||||
|
||||
# these are for our integration tests
|
||||
!.git_keep
|
||||
!.gitmodules_keep
|
||||
|
||||
test/git_server/data
|
||||
test/integration/*/actual/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too much space
|
||||
test/integration/*/expected/**/hooks/
|
||||
test/integration/*/expected_remote/**/hooks/
|
||||
|
||||
test/results/**
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
__debug_bin
|
||||
|
||||
+1
-5
@@ -32,12 +32,8 @@ checksum:
|
||||
snapshot:
|
||||
name_template: '{{ .Tag }}-next'
|
||||
changelog:
|
||||
use: github-native
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^bump'
|
||||
brews:
|
||||
-
|
||||
# Repository to push the tap to.
|
||||
|
||||
Vendored
+2
-2
@@ -7,7 +7,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--debug", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"args": ["--debug", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
@@ -19,7 +19,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["--logs", "--use-config-file=.vscode/debugger_config.yml"],
|
||||
"args": ["--logs", "--use-config-file=${workspaceFolder}/.vscode/debugger_config.yml"],
|
||||
"console": "integratedTerminal",
|
||||
"presentation": {
|
||||
"hidden": true
|
||||
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Generate cheatsheet",
|
||||
"type": "process",
|
||||
"command": "go run scripts/cheatsheet/main.go ",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Bump gocui",
|
||||
"type": "shell",
|
||||
"command": "./scripts/bump_gocui.sh",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Bump lazycore",
|
||||
"type": "shell",
|
||||
"command": "./scripts/bump_lazycore.sh",
|
||||
"problemMatcher": [],
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test (slow)",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --slow ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run current file integration test (sandbox)",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go && go run cmd/integration_test/main.go cli --sandbox ${relativeFile}",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Open deprecated test TUI",
|
||||
"type": "shell",
|
||||
"command": "go run pkg/integration/deprecated/cmd/tui/main.go",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Sync tests list",
|
||||
"type": "shell",
|
||||
"command": "go generate pkg/integration/tests/tests.go",
|
||||
"problemMatcher": [],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
},
|
||||
"presentation": {
|
||||
"focus": true
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
+81
-9
@@ -23,6 +23,28 @@ welcome your pull requests:
|
||||
|
||||
If you've never written Go in your life, then join the club! Lazygit was the maintainer's first Go program, and most contributors have never used Go before. Go is widely considered an easy-to-learn language, so if you're looking for an open source project to gain dev experience, you've come to the right place.
|
||||
|
||||
## Running in a VSCode dev container
|
||||
|
||||
If you want to spare yourself the hassle of setting up your dev environment yourself (i.e. installing Go, extensions, and extra tools), you can run the Lazygit code in a VSCode dev container like so:
|
||||
|
||||

|
||||
|
||||
This requires that:
|
||||
* you have docker installed
|
||||
* you have the dev containers extension installed in VSCode
|
||||
|
||||
See [here](https://code.visualstudio.com/docs/devcontainers/containers) for more info about dev containers.
|
||||
|
||||
## Running in a Github Codespace
|
||||
|
||||
If you want to start contributing to Lazygit with the click of a button, you can open the lazygit codebase in a Codespace. First fork the repo, then click to create a codespace:
|
||||
|
||||

|
||||
|
||||
To run lazygit from within the integrated terminal just go `go run main.go`
|
||||
|
||||
This allows you to contribute to Lazygit without needing to install anything on your local machine. The Codespace has all the necessary tools and extensions pre-installed.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||
@@ -63,15 +85,21 @@ by setting [`formatting.gofumpt`](https://github.com/golang/tools/blob/master/go
|
||||
```jsonc
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To run gofumpt from your terminal go:
|
||||
|
||||
```
|
||||
go install mvdan.cc/gofumpt@latest && gofumpt -l -w .
|
||||
```
|
||||
|
||||
## Internationalisation
|
||||
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
|
||||
Boy that's a hard word to spell. Anyway, lazygit is translated into several languages within the pkg/i18n package. If you need to render text to the user, you should add a new field to the TranslationSet struct in `pkg/i18n/english.go` and add the actual content within the `EnglishTranslationSet()` method in the same file. Then you can access via `gui.Tr.YourNewText` (or `app.Tr.YourNewText`, etc). Although it is appreciated if you translate the text into other languages, it's not expected of you (google translate will likely do a bad job anyway!).
|
||||
|
||||
## Debugging
|
||||
|
||||
@@ -82,6 +110,7 @@ From most places in the codebase you have access to a logger e.g. `gui.Log.Warn(
|
||||
If you find that the existing logs are too noisy, you can set the log level with e.g. `LOG_LEVEL=warn go run main.go -debug` and then only use `Warn` logs yourself.
|
||||
|
||||
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by adding the following:
|
||||
|
||||
```go
|
||||
func newLogger() *logrus.Entry {
|
||||
// REPLACE THE BELOW PATH WITH YOUR ACTUAL LOG PATH (YOU'LL SEE THIS PRINTED WHEN YOU RUN `lazygit --logs`
|
||||
@@ -118,24 +147,50 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": [
|
||||
"--debug"
|
||||
],
|
||||
"args": ["--debug"],
|
||||
"console": "externalTerminal" // <-- you need this to actually see the lazygit UI in a window while debugging
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Profiling
|
||||
|
||||
If you want to investigate what's contributing to CPU usage you can add the following to the top of the `main()` function in `main.go`
|
||||
|
||||
```go
|
||||
import "runtime/pprof"
|
||||
|
||||
func main() {
|
||||
f, err := os.Create("cpu.prof")
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
...
|
||||
```
|
||||
|
||||
Then run lazygit, and afterwards, from your terminal, run:
|
||||
|
||||
```sh
|
||||
go tool pprof --web cpu.prof
|
||||
```
|
||||
|
||||
That should open an application which allows you to view the breakdown of CPU usage.
|
||||
|
||||
## Testing
|
||||
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. Lazygit has its own integration test system where you can build a sandbox repo with a shell script, record yourself doing something, and commit the resulting repo snapshot. It's pretty damn cool! To learn more see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md)
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rendering windows and handling user input. Here's the typical process to follow:
|
||||
|
||||
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
|
||||
1. Make the changes in gocui inside lazygit's vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
|
||||
3. Raise a PR on the gocui repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
|
||||
@@ -146,6 +201,23 @@ Sometimes you will need to make a change in the gocui fork (https://github.com/j
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
## Updating Lazycore
|
||||
|
||||
[Lazycore](https://github.com/jesseduffield/lazycore) is a repo containing shared functionality between lazygit and lazydocker. Sometimes you will need to make a change to that repo and import the changes into lazygit. Similar to updating Gocui, here's what you do:
|
||||
|
||||
1. Make the changes in lazycore inside lazygit's vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual lazycore repo (clone it if you haven't already, and use the `master` branch)
|
||||
3. Raise a PR on the lazycore repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the lazycore version. You can bump the version by running the following at the lazygit repo root:
|
||||
|
||||
```sh
|
||||
./scripts/bump_lazycore.sh
|
||||
```
|
||||
|
||||
Or if you're using VSCode, there is a bump lazycore task you can find by going `cmd+shift+p` and typing 'Run task'
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
## Improvements
|
||||
|
||||
If you can think of any way to improve these docs let us know.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/clients"
|
||||
)
|
||||
|
||||
var usage = `
|
||||
Usage:
|
||||
See https://github.com/jesseduffield/lazygit/tree/master/pkg/integration/README.md
|
||||
|
||||
CLI mode:
|
||||
> go run cmd/integration_test/main.go cli [--slow] [--sandbox] <test1> <test2> ...
|
||||
If you pass no test names, it runs all tests
|
||||
Accepted environment variables:
|
||||
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
|
||||
|
||||
TUI mode:
|
||||
> go run cmd/integration_test/main.go tui
|
||||
This will open up a terminal UI where you can run tests
|
||||
|
||||
Help:
|
||||
> go run cmd/integration_test/main.go help
|
||||
`
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "help":
|
||||
fmt.Println(usage)
|
||||
case "cli":
|
||||
testNames := os.Args[2:]
|
||||
slow := false
|
||||
sandbox := false
|
||||
// get the next arg if it's --slow
|
||||
if len(os.Args) > 2 {
|
||||
if os.Args[2] == "--slow" || os.Args[2] == "-slow" {
|
||||
testNames = os.Args[3:]
|
||||
slow = true
|
||||
} else if os.Args[2] == "--sandbox" || os.Args[2] == "-sandbox" {
|
||||
testNames = os.Args[3:]
|
||||
sandbox = true
|
||||
}
|
||||
}
|
||||
|
||||
clients.RunCLI(testNames, slow, sandbox)
|
||||
case "tui":
|
||||
clients.RunTUI()
|
||||
default:
|
||||
log.Fatal(usage)
|
||||
}
|
||||
}
|
||||
+67
-62
@@ -9,14 +9,19 @@ Default path for the config file:
|
||||
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
|
||||
|
||||
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application\ Support/jesseduffield/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
If you want to change the config directory:
|
||||
|
||||
- MacOS: `export XDG_CONFIG_HOME="$HOME/.config"`
|
||||
|
||||
## Default
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
windowSize: 'normal' # one of 'normal' | 'half' | 'full' default is 'normal'
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
@@ -25,7 +30,6 @@ gui:
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl' | 'ja' | 'ko'
|
||||
timeFormat: '02 Jan 06 15:04 MST' # https://pkg.go.dev/time#Time.Format
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- green
|
||||
- bold
|
||||
@@ -43,6 +47,8 @@ gui:
|
||||
- blue
|
||||
unstagedChangesColor:
|
||||
- red
|
||||
defaultFgColor:
|
||||
- default
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
@@ -51,24 +57,28 @@ gui:
|
||||
showFileTree: true # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
showRandomTip: true
|
||||
experimentalShowBranchHeads: false # visualize branch heads with (*) in commits list
|
||||
showBottomLine: true # for hiding the bottom information line (unless it has important information to tell you)
|
||||
showCommandLog: true
|
||||
showIcons: false
|
||||
commandLogSize: 8
|
||||
splitDiff: 'auto' # one of 'auto' | 'always'
|
||||
skipRewordInEditorWarning: false # for skipping the confirmation before launching the reword editor
|
||||
border: 'single' # one of 'single' | 'double' | 'rounded' | 'hidden'
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
commit:
|
||||
signOff: false
|
||||
verbose: default # one of 'default' | 'always' | 'never'
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
log:
|
||||
# one of date-order, author-date-order, topo-order.
|
||||
# one of date-order, author-date-order, topo-order or default.
|
||||
# topo-order makes it easier to read the git log graph, but commits may not
|
||||
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
|
||||
order: 'topo-order'
|
||||
@@ -78,6 +88,9 @@ git:
|
||||
# displays the whole git graph by default in the commits panel (equivalent to passing the `--all` argument to `git log`)
|
||||
showWholeGraph: false
|
||||
skipHookPrefix: WIP
|
||||
# The main branches. We colour commits green if they belong to one of these branches,
|
||||
# so that you can easily see which commits are unique to your branch (coloured in yellow)
|
||||
mainBranches: [master, main]
|
||||
autoFetch: true
|
||||
autoRefresh: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
@@ -87,21 +100,23 @@ git:
|
||||
parseEmoji: false
|
||||
diffContextSize: 3 # how many lines of context are shown around a change in diffs
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
editCommandTemplate: ''
|
||||
openCommand: ''
|
||||
editPreset: '' # see 'Configuring File Editing' section
|
||||
edit: ''
|
||||
editAtLine: ''
|
||||
editAtLineAndWait: ''
|
||||
open: ''
|
||||
openLink: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # File/submodule refresh interval in seconds. Auto-refresh can be disabled via option 'git.autoRefresh'.
|
||||
fetchInterval: 60 # Re-fetch interval in seconds. Auto-fetch can be disabled via option 'git.autoFetch'.
|
||||
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'
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip' | 'quit'
|
||||
promptToReturnFromSubprocess: true # display confirmation when subprocess terminates
|
||||
keybinding:
|
||||
universal:
|
||||
@@ -127,13 +142,12 @@ keybinding:
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu: null # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
@@ -161,7 +175,6 @@ keybinding:
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
increaseContextInDiffView: '}'
|
||||
@@ -182,6 +195,8 @@ keybinding:
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
openMergeTool: 'M'
|
||||
openStatusFilter: '<c-b>'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
viewPullRequestOptions: 'O'
|
||||
@@ -192,6 +207,7 @@ keybinding:
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
createTag: 'T'
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
@@ -219,6 +235,7 @@ keybinding:
|
||||
viewBisectOptions: 'b'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
renameStash: 'r'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
@@ -238,59 +255,60 @@ keybinding:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'start "" {{filename}}'
|
||||
open: 'start "" {{filename}}'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'xdg-open {{filename}} >/dev/null'
|
||||
open: 'xdg-open {{filename}} >/dev/null'
|
||||
```
|
||||
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
open: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Configuring File Editing
|
||||
|
||||
Lazygit will edit a file with the first set editor in the following:
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o`
|
||||
acts as if the file was double-clicked in the Finder/Explorer, so it also works
|
||||
for non-text files, whereas `e` opens the file in an editor. `e` can also jump
|
||||
to the right line in the file if you invoke it from the staging panel, for
|
||||
example.
|
||||
|
||||
1. config.yaml
|
||||
To tell lazygit which editor to use for the `e` command, the easiest way to do
|
||||
that is to provide an editPreset config, e.g.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim' # as an example
|
||||
editPreset: 'vscode'
|
||||
```
|
||||
|
||||
2. \$(git config core.editor)
|
||||
3. \$GIT_EDITOR
|
||||
4. \$VISUAL
|
||||
5. \$EDITOR
|
||||
6. \$(which vi)
|
||||
Supported presets are `vim`, `nvim`, `emacs`, `nano`, `vscode`, `sublime`, `bbedit`,
|
||||
`kakoune` and `xcode`. In many cases lazygit will be able to guess the right preset
|
||||
from your $(git config core.editor), or an environment variable such as $VISUAL or $EDITOR.
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
You can specify a line number you are currently at when in the line-by-line mode.
|
||||
If for some reason you are not happy with the default commands from a preset, or
|
||||
there simply is no preset for your editor, you can customize the commands by
|
||||
setting the `edit`, `editAtLine`, and `editAtLineAndWait` options, e.g.:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim'
|
||||
editCommandTemplate: '{{editor}} +{{line}} -- {{filename}}'
|
||||
edit: 'myeditor {{filename}}'
|
||||
editAtLine: 'myeditor --line={{line}} {{filename}}'
|
||||
editAtLineAndWait: 'myeditor --block --line={{line}} {{filename}}'
|
||||
editInTerminal: true
|
||||
```
|
||||
|
||||
or
|
||||
The `editInTerminal` option is used to decide whether lazygit needs to suspend
|
||||
itself to the background before calling the editor.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'code'
|
||||
editCommandTemplate: '{{editor}} --goto -- {{filename}}:{{line}}'
|
||||
```
|
||||
|
||||
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||
Contributions of new editor presets are welcome; see the `getPreset` function in
|
||||
[`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go).
|
||||
|
||||
### Overriding default config file location
|
||||
|
||||
@@ -306,15 +324,6 @@ or
|
||||
LG_CONFIG_FILE="$HOME/.base_lg_conf,$HOME/.light_theme_lg_conf" lazygit
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
@@ -339,23 +348,6 @@ The available attributes are:
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Highlighting the selected line
|
||||
|
||||
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` and `selectedRangeBgColor` keys to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
|
||||
@@ -431,6 +423,14 @@ gui:
|
||||
|
||||
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
|
||||
|
||||
You can disable certain key bindings by specifying `null`.
|
||||
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
edit: null # disable 'edit file'
|
||||
```
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
@@ -477,7 +477,7 @@ services:
|
||||
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`, `bitbucketServer`, `azuredevops` or `gitlab`
|
||||
- `provider` is one of `github`, `bitbucket`, `bitbucketServer`, `azuredevops`, `gitlab` or `gitea`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
@@ -531,3 +531,8 @@ notARepository: 'create'
|
||||
# to skip without creating a new repo
|
||||
notARepository: 'skip'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to exit immediately if run outside of the Git repository
|
||||
notARepository: 'quit'
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ customCommands:
|
||||
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
|
||||
context: 'commits'
|
||||
- key: 'a'
|
||||
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name}}"
|
||||
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name | quote}}"
|
||||
context: 'files'
|
||||
description: 'toggle file staged'
|
||||
- key: 'C'
|
||||
@@ -77,6 +77,7 @@ For a given custom command, here are the allowed fields:
|
||||
| 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 |
|
||||
| stream | whether you want to stream the command's output to the Command Log panel | no |
|
||||
| showOutput | whether you want to show the command's output in a gui prompt | no |
|
||||
|
||||
### Contexts
|
||||
|
||||
@@ -135,13 +136,14 @@ If an option has no name the value will be displayed to the user in place of the
|
||||
|
||||
### 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:
|
||||
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/golang/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
|
||||
SelectedPath
|
||||
SelectedLocalBranch
|
||||
SelectedRemoteBranch
|
||||
SelectedRemote
|
||||
@@ -159,7 +161,7 @@ If your custom keybinding collides with an inbuilt keybinding that is defined fo
|
||||
|
||||
### 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
|
||||
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `showOutput: 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
|
||||
|
||||
|
||||
+1
-122
@@ -1,122 +1 @@
|
||||
# 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"
|
||||
```
|
||||
|
||||
Be sure to:
|
||||
|
||||
- ensure that by the end of the test you've got at least one commit in the repo, as we've had issues in the past when that wasn't the case.
|
||||
- set the git user email and name as above so that your own user details aren't included in the snapshot.
|
||||
|
||||
## Running tests
|
||||
|
||||
### From a TUI
|
||||
|
||||
You can run/record/sandbox tests via a TUI with the following command:
|
||||
|
||||
```
|
||||
go run test/lazyintegration/main.go
|
||||
```
|
||||
|
||||
This TUI makes much of the following documentation redundant, but feel free to read through anyway!
|
||||
|
||||
### From command line
|
||||
|
||||
To run all tests - assuming you're at the project root:
|
||||
|
||||
```
|
||||
go test ./pkg/gui/
|
||||
```
|
||||
|
||||
To run them in parallel
|
||||
|
||||
```
|
||||
PARALLEL=true go test ./pkg/gui
|
||||
```
|
||||
|
||||
To run a single test
|
||||
|
||||
```
|
||||
go test ./pkg/gui -run /<test name>
|
||||
# For example, to run the `tags` test:
|
||||
go test ./pkg/gui -run /tags
|
||||
```
|
||||
|
||||
To run a test at a certain speed
|
||||
|
||||
```
|
||||
SPEED=2 go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
To update a snapshot
|
||||
|
||||
```
|
||||
MODE=updateSnapshot go test ./pkg/gui -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:
|
||||
|
||||
```
|
||||
MODE=record go test ./pkg/gui -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(s) after running the test, ignored by git)
|
||||
expected/ (the 'snapshot' repo(s))
|
||||
config/ (need not be present)
|
||||
test.json
|
||||
setup.sh
|
||||
recording.json
|
||||
```
|
||||
|
||||
## Sandboxing
|
||||
|
||||
The integration tests serve a secondary purpose of providing a setup for easy sandboxing. If you want to run a test in sandbox mode (meaning the session won't be recorded and we won't create/update snapshots), go:
|
||||
|
||||
```
|
||||
MODE=sandbox go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
## Feedback
|
||||
|
||||
If you think this process can be improved, let me know! It shouldn't be too hard to change things.
|
||||
see new docs [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>?</kbd>: open menu
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
@@ -30,11 +31,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
@@ -55,6 +56,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commit Summary
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: confirm
|
||||
<kbd>esc</kbd>: close
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
@@ -88,11 +96,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Confirmation Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: confirm
|
||||
<kbd>esc</kbd>: close/cancel
|
||||
</pre>
|
||||
|
||||
## Files
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
@@ -102,7 +116,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>s</kbd>: stash all changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
@@ -131,6 +145,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
@@ -140,60 +155,67 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Main Panel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>◀</kbd>: select previous conflict
|
||||
<kbd>▶</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>z</kbd>: undo
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: scroll down (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: scroll up (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: scroll down (fn+up)
|
||||
<kbd>mouse wheel ▲</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>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>◀</kbd>: select previous hunk
|
||||
<kbd>▶</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</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>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>◀</kbd>: select previous hunk
|
||||
<kbd>▶</kbd>: select next hunk
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit 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
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: execute
|
||||
<kbd>esc</kbd>: close
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
@@ -214,6 +236,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
@@ -241,14 +264,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>pgup</kbd>: メインパネルを上にスクロール (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: メインパネルを下にスクロール (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: リフレッシュ
|
||||
<kbd>x</kbd>: メニューを開く
|
||||
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 前のスクリーンモード
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: 差分メニューを開く
|
||||
<kbd>ctrl+e</kbd>: 差分メニューを開く
|
||||
<kbd>@</kbd>: コマンドログメニューを開く
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: カスタムコマンドを実行
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>R</kbd>: リフレッシュ
|
||||
<kbd>+</kbd>: 次のスクリーンモード (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 前のスクリーンモード
|
||||
<kbd>?</kbd>: メニューを開く
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: 差分メニューを開く
|
||||
<kbd>ctrl+e</kbd>: 差分メニューを開く
|
||||
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
|
||||
<kbd>z</kbd>: アンドゥ (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: リドゥ (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
@@ -30,11 +31,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 一覧パネルの操作
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: 次のページ
|
||||
<kbd>,</kbd>: 前のページ
|
||||
<kbd>.</kbd>: 次のページ
|
||||
<kbd><</kbd>: 最上部までスクロール
|
||||
<kbd>></kbd>: 最下部までスクロール
|
||||
<kbd>/</kbd>: 検索を開始
|
||||
<kbd>></kbd>: 最下部までスクロール
|
||||
<kbd>H</kbd>: 左スクロール
|
||||
<kbd>L</kbd>: 右スクロール
|
||||
<kbd>]</kbd>: 次のタブ
|
||||
@@ -48,6 +49,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>r</kbd>: Stashを変更
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
@@ -113,6 +115,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>`</kbd>: ファイルツリーの表示を切り替え
|
||||
</pre>
|
||||
|
||||
## コミットメッセージ
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 確認
|
||||
<kbd>esc</kbd>: 閉じる
|
||||
</pre>
|
||||
|
||||
## サブモジュール
|
||||
|
||||
<pre>
|
||||
@@ -129,8 +138,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## ステータス
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 設定ファイルを編集
|
||||
<kbd>o</kbd>: 設定ファイルを開く
|
||||
<kbd>e</kbd>: 設定ファイルを編集
|
||||
<kbd>u</kbd>: 更新を確認
|
||||
<kbd>enter</kbd>: 最近使用したリポジトリに切り替え
|
||||
<kbd>a</kbd>: すべてのブランチログを表示
|
||||
@@ -151,7 +160,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ファイル名をクリップボードにコピー
|
||||
<kbd>ctrl+w</kbd>: 空白文字の差分の表示有無を切り替え
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: ステージ/アンステージ
|
||||
<kbd>ctrl+b</kbd>: ファイルをフィルタ (ステージ/アンステージ)
|
||||
@@ -190,6 +198,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: タグを作成
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: ブランチ名を変更
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
@@ -199,60 +208,67 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## メインパネル (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>◀</kbd>: 前のコンフリクトを選択
|
||||
<kbd>▶</kbd>: 次のコンフリクトを選択
|
||||
<kbd>▲</kbd>: 前のhunkを選択
|
||||
<kbd>▼</kbd>: 次のhunkを選択
|
||||
<kbd>z</kbd>: アンドゥ
|
||||
<kbd>M</kbd>: git mergetoolを開く
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: 前のコンフリクトを選択
|
||||
<kbd>►</kbd>: 次のコンフリクトを選択
|
||||
<kbd>▲</kbd>: 前のhunkを選択
|
||||
<kbd>▼</kbd>: 次のhunkを選択
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>z</kbd>: アンドゥ
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
</pre>
|
||||
|
||||
## メインパネル (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: 下にスクロール (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 上にスクロール (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: 下にスクロール (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 上にスクロール (fn+down)
|
||||
</pre>
|
||||
|
||||
## メインパネル (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: line-by-lineモードを終了
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>▲</kbd>: 前の行を選択
|
||||
<kbd>▼</kbd>: 次の行を選択
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>space</kbd>: 行をパッチに追加/削除
|
||||
<kbd>◀</kbd>: 前のhunkを選択
|
||||
<kbd>▶</kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>space</kbd>: 行をパッチに追加/削除
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## メインパネル (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>space</kbd>: 選択行をステージ/アンステージ
|
||||
<kbd>d</kbd>: 変更を削除 (git reset)
|
||||
<kbd>tab</kbd>: パネルを切り替え
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>▲</kbd>: 前の行を選択
|
||||
<kbd>▼</kbd>: 次の行を選択
|
||||
<kbd>◄</kbd>: 前のhunkを選択
|
||||
<kbd>►</kbd>: 次のhunkを選択
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>◀</kbd>: 前のhunkを選択
|
||||
<kbd>▶</kbd>: 次のhunkを選択
|
||||
<kbd>v</kbd>: 範囲選択を切り替え
|
||||
<kbd>V</kbd>: 範囲選択を切り替え
|
||||
<kbd>a</kbd>: hunk選択を切り替え
|
||||
<kbd>ctrl+o</kbd>: 選択されたテキストをクリップボードにコピー
|
||||
<kbd>o</kbd>: ファイルを開く
|
||||
<kbd>e</kbd>: ファイルを編集
|
||||
<kbd>esc</kbd>: ファイル一覧に戻る
|
||||
<kbd>tab</kbd>: パネルを切り替え
|
||||
<kbd>space</kbd>: 選択行をステージ/アンステージ
|
||||
<kbd>d</kbd>: 変更を削除 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: 変更をコミット
|
||||
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
|
||||
<kbd>C</kbd>: gitエディタを使用して変更をコミット
|
||||
</pre>
|
||||
|
||||
## メニュー
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 実行
|
||||
<kbd>esc</kbd>: 閉じる
|
||||
</pre>
|
||||
|
||||
## リモート
|
||||
@@ -267,6 +283,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## リモートブランチ
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: ブランチ名をクリップボードにコピー
|
||||
<kbd>space</kbd>: チェックアウト
|
||||
<kbd>n</kbd>: 新しいブランチを作成
|
||||
<kbd>M</kbd>: 現在のブランチにマージ
|
||||
@@ -292,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>enter</kbd>: コミットを閲覧
|
||||
</pre>
|
||||
|
||||
## 確認パネル
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 確認
|
||||
<kbd>esc</kbd>: 閉じる/キャンセル
|
||||
</pre>
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>pgup</kbd>: 메인 패널을 위로 스크롤 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 메인 패널을 아래로로 스크롤 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
|
||||
<kbd>R</kbd>: 새로고침
|
||||
<kbd>x</kbd>: 매뉴 열기
|
||||
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 이전 스크린 모드
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: Diff 메뉴 열기
|
||||
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
|
||||
<kbd>@</kbd>: 명령어 로그 메뉴 열기
|
||||
<kbd>}</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기
|
||||
<kbd>{</kbd>: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>ctrl+p</kbd>: 커스텀 Patch 옵션 보기
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>R</kbd>: 새로고침
|
||||
<kbd>+</kbd>: 다음 스크린 모드 (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: 이전 스크린 모드
|
||||
<kbd>?</kbd>: 매뉴 열기
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: Diff 메뉴 열기
|
||||
<kbd>ctrl+e</kbd>: Diff 메뉴 열기
|
||||
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
|
||||
<kbd>z</kbd>: 되돌리기 (reflog) (실험적)
|
||||
<kbd>ctrl+z</kbd>: 다시 실행 (reflog) (실험적)
|
||||
<kbd>P</kbd>: 푸시
|
||||
@@ -30,11 +31,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: 다음 페이지
|
||||
<kbd>,</kbd>: 이전 페이지
|
||||
<kbd>.</kbd>: 다음 페이지
|
||||
<kbd><</kbd>: 맨 위로 스크롤
|
||||
<kbd>></kbd>: 맨 아래로 스크롤
|
||||
<kbd>/</kbd>: 검색 시작
|
||||
<kbd>></kbd>: 맨 아래로 스크롤
|
||||
<kbd>H</kbd>: 우 스크롤
|
||||
<kbd>L</kbd>: 좌 스크롤
|
||||
<kbd>]</kbd>: 이전 탭
|
||||
@@ -63,6 +64,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
@@ -81,63 +83,70 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>enter</kbd>: view selected item's files
|
||||
</pre>
|
||||
|
||||
## 메뉴
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 실행
|
||||
<kbd>esc</kbd>: 닫기
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>◀</kbd>: 이전 충돌을 선택
|
||||
<kbd>▶</kbd>: 다음 충돌을 선택
|
||||
<kbd>▲</kbd>: 이전 hunk를 선택
|
||||
<kbd>▼</kbd>: 다음 hunk를 선택
|
||||
<kbd>z</kbd>: 되돌리기
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: 이전 충돌을 선택
|
||||
<kbd>►</kbd>: 다음 충돌을 선택
|
||||
<kbd>▲</kbd>: 이전 hunk를 선택
|
||||
<kbd>▼</kbd>: 다음 hunk를 선택
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>z</kbd>: 되돌리기
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: 아래로 스크롤 (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 위로 스크롤 (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: 아래로 스크롤 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 위로 스크롤 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: line-by-line 모드 종료
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>▲</kbd>: 이전 줄 선택
|
||||
<kbd>▼</kbd>: 다음 줄 선택
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
|
||||
<kbd>◀</kbd>: 이전 hunk를 선택
|
||||
<kbd>▶</kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>space</kbd>: line(s)을 패치에 추가/삭제
|
||||
<kbd>esc</kbd>: exit custom patch builder
|
||||
</pre>
|
||||
|
||||
## 메인 패널 (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>space</kbd>: 선택한 행을 staged / unstaged
|
||||
<kbd>d</kbd>: 변경을 삭제 (git reset)
|
||||
<kbd>tab</kbd>: 패널 전환
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>▲</kbd>: 이전 줄 선택
|
||||
<kbd>▼</kbd>: 다음 줄 선택
|
||||
<kbd>◄</kbd>: 이전 hunk를 선택
|
||||
<kbd>►</kbd>: 다음 hunk를 선택
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>◀</kbd>: 이전 hunk를 선택
|
||||
<kbd>▶</kbd>: 다음 hunk를 선택
|
||||
<kbd>v</kbd>: 드래그 선택 전환
|
||||
<kbd>V</kbd>: 드래그 선택 전환
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: 선택한 텍스트를 클립보드에 복사
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>esc</kbd>: 파일 목록으로 돌아가기
|
||||
<kbd>tab</kbd>: 패널 전환
|
||||
<kbd>space</kbd>: 선택한 행을 staged / unstaged
|
||||
<kbd>d</kbd>: 변경을 삭제 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: 커밋 변경내용
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
</pre>
|
||||
|
||||
## 브랜치
|
||||
@@ -156,6 +165,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: 태그를 생성
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: 브랜치 이름 변경
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
@@ -165,8 +175,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 상태
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 설정 파일 수정
|
||||
<kbd>o</kbd>: 설정 파일 열기
|
||||
<kbd>e</kbd>: 설정 파일 수정
|
||||
<kbd>u</kbd>: 업데이트 확인
|
||||
<kbd>enter</kbd>: 최근에 사용한 저장소로 전환
|
||||
<kbd>a</kbd>: 모든 브랜치 로그 표시
|
||||
@@ -197,6 +207,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 원격 브랜치
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 브랜치명을 클립보드에 복사
|
||||
<kbd>space</kbd>: 체크아웃
|
||||
<kbd>n</kbd>: 새 브랜치 생성
|
||||
<kbd>M</kbd>: 현재 브랜치에 병합
|
||||
@@ -255,6 +266,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>`</kbd>: 파일 트리뷰로 전환
|
||||
</pre>
|
||||
|
||||
## 커밋메시지
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 확인
|
||||
<kbd>esc</kbd>: 닫기
|
||||
</pre>
|
||||
|
||||
## 태그
|
||||
|
||||
<pre>
|
||||
@@ -270,7 +288,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 파일명을 클립보드에 복사
|
||||
<kbd>ctrl+w</kbd>: 공백문자를 Diff 뷰에서 표시 여부 전환
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>space</kbd>: Staged 전환
|
||||
<kbd>ctrl+b</kbd>: 파일을 필터하기 (Staged/unstaged)
|
||||
@@ -280,7 +297,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
|
||||
<kbd>e</kbd>: 파일 편집
|
||||
<kbd>o</kbd>: 파일 닫기
|
||||
<kbd>i</kbd>: Ignore file
|
||||
<kbd>i</kbd>: ignore file
|
||||
<kbd>r</kbd>: 파일 새로고침
|
||||
<kbd>s</kbd>: 변경사항을 Stash
|
||||
<kbd>S</kbd>: Stash 옵션 보기
|
||||
@@ -292,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>M</kbd>: git mergetool를 열기
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## 확인 패널
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 확인
|
||||
<kbd>esc</kbd>: 닫기/취소
|
||||
</pre>
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: voer aangepaste commando uit
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>?</kbd>: open menu
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>P</kbd>: push
|
||||
@@ -30,11 +31,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Lijstpaneel Navigatie
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoeken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: volgende tabblad
|
||||
@@ -45,7 +46,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
@@ -55,7 +55,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
@@ -68,6 +68,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>f</kbd>: fetch
|
||||
</pre>
|
||||
|
||||
## Bevestigingspaneel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bevestig
|
||||
<kbd>esc</kbd>: sluiten
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
|
||||
<pre>
|
||||
@@ -84,12 +91,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
|
||||
<kbd>T</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Commit Bericht
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: bevestig
|
||||
<kbd>esc</kbd>: sluiten
|
||||
</pre>
|
||||
|
||||
## Commit bestanden
|
||||
|
||||
<pre>
|
||||
@@ -137,43 +152,49 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Menu
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: uitvoeren
|
||||
<kbd>esc</kbd>: sluiten
|
||||
</pre>
|
||||
|
||||
## Mergen
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<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
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<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>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
</pre>
|
||||
|
||||
## Normaal
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: scroll omhoog (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Patch Bouwen
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
<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>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>◀</kbd>: selecteer de vorige hunk
|
||||
<kbd>▶</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
@@ -194,6 +215,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
@@ -217,21 +239,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 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>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>◀</kbd>: selecteer de vorige hunk
|
||||
<kbd>▶</kbd>: selecteer de volgende hunk
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>tab</kbd>: ga naar een ander paneel
|
||||
<kbd>space</kbd>: toggle lijnen staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert change (git reset)
|
||||
<kbd>E</kbd>: edit 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>
|
||||
|
||||
## Stash
|
||||
@@ -241,14 +264,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>e</kbd>: verander config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down/shift+j)
|
||||
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
<kbd>}</kbd>: Increase the size of the context shown around changes in the diff view
|
||||
<kbd>{</kbd>: Decrease the size of the context shown around changes in the diff view
|
||||
<kbd>:</kbd>: wykonaj własną komendę
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>m</kbd>: widok scalenia/opcje zmiany bazy
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>?</kbd>: open menu
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>P</kbd>: push
|
||||
@@ -30,17 +31,24 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>H</kbd>: scroll left
|
||||
<kbd>L</kbd>: scroll right
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Commit Summary
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: potwierdź
|
||||
<kbd>esc</kbd>: zamknij
|
||||
</pre>
|
||||
|
||||
## Commity
|
||||
|
||||
<pre>
|
||||
@@ -74,6 +82,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Confirmation Panel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: potwierdź
|
||||
<kbd>esc</kbd>: zamknij
|
||||
</pre>
|
||||
|
||||
## Local Branches
|
||||
|
||||
<pre>
|
||||
@@ -90,6 +105,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: zmiana bazy gałęzi
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>T</kbd>: create tag
|
||||
<kbd>g</kbd>: wyświetl opcje resetu
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
@@ -99,24 +115,29 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wyście z trybu "linia po linii"
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>▲</kbd>: poprzednia linia
|
||||
<kbd>▼</kbd>: następna linia
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>◀</kbd>: poprzedni kawałek
|
||||
<kbd>▶</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>esc</kbd>: wyście z trybu "linia po linii"
|
||||
</pre>
|
||||
|
||||
## Menu
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: wykonaj
|
||||
<kbd>esc</kbd>: zamknij
|
||||
</pre>
|
||||
|
||||
## Pliki
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
<kbd>d</kbd>: pokaż opcje porzucania zmian
|
||||
<kbd>space</kbd>: przełącz stan poczekalni
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
@@ -126,7 +147,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: Ignore or Exclude file
|
||||
<kbd>i</kbd>: ignore or exclude file
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj zmiany
|
||||
<kbd>S</kbd>: wyświetl opcje schowka
|
||||
@@ -156,21 +177,22 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Poczekalnia
|
||||
|
||||
<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>: poprzednia linia
|
||||
<kbd>▼</kbd>: następna linia
|
||||
<kbd>◄</kbd>: poprzedni kawałek
|
||||
<kbd>►</kbd>: następny kawałek
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>◀</kbd>: poprzedni kawałek
|
||||
<kbd>▶</kbd>: następny kawałek
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>ctrl+o</kbd>: copy the selected text to the clipboard
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>tab</kbd>: switch to other panel (staged/unstaged changes)
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: Zatwierdź zmiany
|
||||
<kbd>w</kbd>: zatwierdź zmiany bez skryptu pre-commit
|
||||
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
|
||||
</pre>
|
||||
|
||||
## Reflog
|
||||
@@ -191,6 +213,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Remote Branches
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
@@ -214,17 +237,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Scalanie
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>◀</kbd>: poprzedni konflikt
|
||||
<kbd>▶</kbd>: następny konflikt
|
||||
<kbd>▲</kbd>: wybierz poprzedni kawałek
|
||||
<kbd>▼</kbd>: wybierz następny kawałek
|
||||
<kbd>z</kbd>: cofnij
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: wybierz kawałek
|
||||
<kbd>b</kbd>: wybierz wszystkie kawałki
|
||||
<kbd>◄</kbd>: poprzedni konflikt
|
||||
<kbd>►</kbd>: następny konflikt
|
||||
<kbd>▲</kbd>: wybierz poprzedni kawałek
|
||||
<kbd>▼</kbd>: wybierz następny kawałek
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>z</kbd>: cofnij
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
</pre>
|
||||
|
||||
## Schowek
|
||||
@@ -234,14 +257,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: przeglądaj pliki commita
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj konfigurację
|
||||
<kbd>o</kbd>: otwórz konfigurację
|
||||
<kbd>e</kbd>: edytuj konfigurację
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokaż wszystkie logi gałęzi
|
||||
@@ -289,6 +313,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## Zwykłe
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: przewiń w dół (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: przewiń w górę (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: przewiń w dół (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: przewiń w górę (fn+down)
|
||||
</pre>
|
||||
|
||||
@@ -8,19 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>ctrl+r</kbd>: 切换到最近的仓库
|
||||
<kbd>pgup</kbd>: 向上滚动主面板 (fn+up/shift+k)
|
||||
<kbd>pgdown</kbd>: 向下滚动主面板 (fn+down/shift+j)
|
||||
<kbd>m</kbd>: 查看 合并/变基 选项
|
||||
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
|
||||
<kbd>R</kbd>: 刷新
|
||||
<kbd>x</kbd>: 打开菜单
|
||||
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
|
||||
<kbd>_</kbd>: 上一屏模式
|
||||
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
|
||||
<kbd>W</kbd>: 打开 diff 菜单
|
||||
<kbd>ctrl+e</kbd>: 打开 diff 菜单
|
||||
<kbd>@</kbd>: 打开命令日志菜单
|
||||
<kbd>}</kbd>: 扩大差异视图中显示的上下文范围
|
||||
<kbd>{</kbd>: 缩小差异视图中显示的上下文范围
|
||||
<kbd>:</kbd>: 执行自定义命令
|
||||
<kbd>ctrl+p</kbd>: 查看自定义补丁选项
|
||||
<kbd>m</kbd>: 查看 合并/变基 选项
|
||||
<kbd>R</kbd>: 刷新
|
||||
<kbd>+</kbd>: 下一屏模式(正常/半屏/全屏)
|
||||
<kbd>_</kbd>: 上一屏模式
|
||||
<kbd>?</kbd>: 打开菜单
|
||||
<kbd>ctrl+s</kbd>: 查看按路径过滤选项
|
||||
<kbd>W</kbd>: 打开 diff 菜单
|
||||
<kbd>ctrl+e</kbd>: 打开 diff 菜单
|
||||
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
|
||||
<kbd>z</kbd>: (通过 reflog)撤销「实验功能」
|
||||
<kbd>ctrl+z</kbd>: (通过 reflog)重做「实验功能」
|
||||
<kbd>P</kbd>: 推送
|
||||
@@ -30,11 +31,11 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 列表面板导航
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: 下一页
|
||||
<kbd>,</kbd>: 上一页
|
||||
<kbd>.</kbd>: 下一页
|
||||
<kbd><</kbd>: 滚动到顶部
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>/</kbd>: 开始搜索
|
||||
<kbd>></kbd>: 滚动到底部
|
||||
<kbd>H</kbd>: 向左滚动
|
||||
<kbd>L</kbd>: 向右滚动
|
||||
<kbd>]</kbd>: 下一个标签
|
||||
@@ -72,6 +73,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>r</kbd>: 将已检出的分支变基到该分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
<kbd>f</kbd>: 从上游快进此分支
|
||||
<kbd>T</kbd>: 创建标签
|
||||
<kbd>g</kbd>: 查看重置选项
|
||||
<kbd>R</kbd>: 重命名分支
|
||||
<kbd>u</kbd>: set/unset upstream
|
||||
@@ -153,11 +155,17 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>`</kbd>: 切换文件树视图
|
||||
</pre>
|
||||
|
||||
## 提交讯息
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 确认
|
||||
<kbd>esc</kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 文件
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将文件名复制到剪贴板
|
||||
<kbd>ctrl+w</kbd>: 切换是否在差异视图中显示空白字符差异
|
||||
<kbd>d</kbd>: 查看'放弃更改'选项
|
||||
<kbd>space</kbd>: 切换暂存状态
|
||||
<kbd>ctrl+b</kbd>: Filter files (staged/unstaged)
|
||||
@@ -183,17 +191,16 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 构建补丁中
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 退出逐行模式
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>▲</kbd>: 选择上一行
|
||||
<kbd>▼</kbd>: 选择下一行
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>space</kbd>: 添加/移除 行到补丁
|
||||
<kbd>◀</kbd>: 选择上一个区块
|
||||
<kbd>▶</kbd>: 选择下一个区块
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>space</kbd>: 添加/移除 行到补丁
|
||||
<kbd>esc</kbd>: 退出逐行模式
|
||||
</pre>
|
||||
|
||||
## 标签页面
|
||||
@@ -210,56 +217,71 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
## 正在合并
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>◀</kbd>: 选择上一个冲突
|
||||
<kbd>▶</kbd>: 选择下一个冲突
|
||||
<kbd>▲</kbd>: 选择顶部块
|
||||
<kbd>▼</kbd>: 选择底部块
|
||||
<kbd>z</kbd>: 撤销
|
||||
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
|
||||
<kbd>space</kbd>: 选中区块
|
||||
<kbd>b</kbd>: 选中所有区块
|
||||
<kbd>◄</kbd>: 选择上一个冲突
|
||||
<kbd>►</kbd>: 选择下一个冲突
|
||||
<kbd>▲</kbd>: 选择顶部块
|
||||
<kbd>▼</kbd>: 选择底部块
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>z</kbd>: 撤销
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
</pre>
|
||||
|
||||
## 正在暂存
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>space</kbd>: 切换行暂存状态
|
||||
<kbd>d</kbd>: 取消变更 (git reset)
|
||||
<kbd>tab</kbd>: 切换到其他面板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>▲</kbd>: 选择上一行
|
||||
<kbd>▼</kbd>: 选择下一行
|
||||
<kbd>◄</kbd>: 选择上一个区块
|
||||
<kbd>►</kbd>: 选择下一个区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>◀</kbd>: 选择上一个区块
|
||||
<kbd>▶</kbd>: 选择下一个区块
|
||||
<kbd>v</kbd>: 切换拖动选择
|
||||
<kbd>V</kbd>: 切换拖动选择
|
||||
<kbd>a</kbd>: 切换选择区块
|
||||
<kbd>ctrl+o</kbd>: 将选中文本复制到剪贴板
|
||||
<kbd>o</kbd>: 打开文件
|
||||
<kbd>e</kbd>: 编辑文件
|
||||
<kbd>esc</kbd>: 返回文件面板
|
||||
<kbd>tab</kbd>: 切换到其他面板
|
||||
<kbd>space</kbd>: 切换行暂存状态
|
||||
<kbd>d</kbd>: 取消变更 (git reset)
|
||||
<kbd>E</kbd>: edit hunk
|
||||
<kbd>c</kbd>: 提交更改
|
||||
<kbd>w</kbd>: 提交更改而无需预先提交钩子
|
||||
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
|
||||
</pre>
|
||||
|
||||
## 正常
|
||||
|
||||
<pre>
|
||||
<kbd>mouse wheel down</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>mouse wheel up</kbd>: 向上滚动 (fn+down)
|
||||
<kbd>mouse wheel ▼</kbd>: 向下滚动 (fn+up)
|
||||
<kbd>mouse wheel ▲</kbd>: 向上滚动 (fn+down)
|
||||
</pre>
|
||||
|
||||
## 状态
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: 编辑配置文件
|
||||
<kbd>o</kbd>: 打开配置文件
|
||||
<kbd>e</kbd>: 编辑配置文件
|
||||
<kbd>u</kbd>: 检查更新
|
||||
<kbd>enter</kbd>: 切换到最近的仓库
|
||||
<kbd>a</kbd>: 显示所有分支的日志
|
||||
</pre>
|
||||
|
||||
## 确认面板
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 确认
|
||||
<kbd>esc</kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 菜单
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: 执行
|
||||
<kbd>esc</kbd>: 关闭
|
||||
</pre>
|
||||
|
||||
## 贮藏
|
||||
|
||||
<pre>
|
||||
@@ -267,12 +289,14 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
<kbd>g</kbd>: 应用并删除
|
||||
<kbd>d</kbd>: 删除
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>r</kbd>: rename stash
|
||||
<kbd>enter</kbd>: 查看提交的文件
|
||||
</pre>
|
||||
|
||||
## 远程分支
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: 将分支名称复制到剪贴板
|
||||
<kbd>space</kbd>: 检出
|
||||
<kbd>n</kbd>: 新分支
|
||||
<kbd>M</kbd>: 合并到当前检出的分支
|
||||
|
||||
@@ -4,37 +4,41 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
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/creack/pty v1.1.11
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.6.0
|
||||
github.com/go-errors/errors v1.4.2
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/kyokomi/emoji/v2 v2.2.8
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mattn/go-runewidth v0.0.14
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/samber/lo v1.10.1
|
||||
github.com/samber/lo v1.31.0
|
||||
github.com/sanity-io/litter v1.5.2
|
||||
github.com/sasha-s/go-deadlock v0.3.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -42,7 +46,6 @@ require (
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.0.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
@@ -58,15 +61,15 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
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/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/term v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
|
||||
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/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/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=
|
||||
@@ -28,15 +30,15 @@ github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9
|
||||
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/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2 h1:l6Y+9q7jbM+yK/w6kASpHO7ejL9ARCErm3tCEqOT278=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.2/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4 h1:fzcGaoAFDHWzJRKw//CSZFrXucsLKplIvOSab3FtWWM=
|
||||
github.com/fsmiamoto/git-todo-parser v0.0.4/go.mod h1:B+AgTbNE2BARvJqzXygThzqxLIaEWvwr2sxKYYb0Fas=
|
||||
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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I=
|
||||
github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo=
|
||||
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
|
||||
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
|
||||
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=
|
||||
@@ -70,12 +72,14 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8TIcC6Y4RI+1ZbJDOHfGJ570tPeYVCqo7/tws=
|
||||
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
|
||||
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.20220417002912-bce22fd599f6 h1:Fmay0Lz21taUpXiIbFkjjIIcn0E5GKwp5UFRuXaOiGQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20220417002912-bce22fd599f6/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa h1:E9G1mj94rMal1YLaABwdxLUUgKq+xGbElFjHRNaDJUg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20230324073941-36f2e87458fa/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
|
||||
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
@@ -112,8 +116,8 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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 v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@@ -125,20 +129,26 @@ 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/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/samber/lo v1.10.1 h1:0D3h7i0U3hRAbaCeQ82DLe67n0A7Bbl0/cEoWqFGp+U=
|
||||
github.com/samber/lo v1.10.1/go.mod h1:2I7tgIv8Q1SG2xEIkRq0F2i2zgxVpnyPOP0d3Gj2r+A=
|
||||
github.com/samber/lo v1.31.0 h1:Sfa+/064Tdo4SvlohQUQzBhgSer9v/coGvKQI/XLWAM=
|
||||
github.com/samber/lo v1.31.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8=
|
||||
github.com/sanity-io/litter v1.5.2 h1:AnC8s9BMORWH5a4atZ4D6FPVvKGzHcnc5/IVTa87myw=
|
||||
github.com/sanity-io/litter v1.5.2/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0=
|
||||
github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM=
|
||||
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=
|
||||
@@ -146,34 +156,42 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
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/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/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
|
||||
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=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
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/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/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 h1:s/+U+w0teGzcoH2mdIlFQ6KfVKGaYpgyGdUefZrn9TU=
|
||||
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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=
|
||||
@@ -183,22 +201,33 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -217,5 +246,5 @@ 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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,179 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
|
||||
// These values may be set by the build script via the LDFLAGS argument
|
||||
var (
|
||||
commit string
|
||||
version = "unversioned"
|
||||
date string
|
||||
version string
|
||||
buildSource = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
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")
|
||||
|
||||
customConfig := ""
|
||||
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if repoPath != "" {
|
||||
if workTree != "" || gitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(repoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
workTree = absRepoPath
|
||||
gitDir = filepath.Join(absRepoPath, ".git")
|
||||
ldFlagsBuildInfo := &app.BuildInfo{
|
||||
Commit: commit,
|
||||
Date: date,
|
||||
Version: version,
|
||||
BuildSource: buildSource,
|
||||
}
|
||||
|
||||
if customConfig != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
if err := os.Chdir(workTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
common, err := app.NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(gitArg)
|
||||
|
||||
app.Run(appConfig, common, types.NewStartArgs(filterPath, parsedGitArg))
|
||||
}
|
||||
|
||||
func parseGitArg(gitArg string) types.GitArg {
|
||||
typedArg := types.GitArg(gitArg)
|
||||
|
||||
// using switch so that linter catches when a new git arg value is defined but not handled here
|
||||
switch typedArg {
|
||||
case types.GitArgNone, types.GitArgStatus, types.GitArgBranch, types.GitArgLog, types.GitArgStash:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
string(types.GitArgStatus),
|
||||
string(types.GitArgBranch),
|
||||
string(types.GitArgLog),
|
||||
string(types.GitArgStash),
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
app.Start(ldFlagsBuildInfo, nil)
|
||||
}
|
||||
|
||||
+89
-67
@@ -7,22 +7,20 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
)
|
||||
@@ -35,10 +33,13 @@ type App struct {
|
||||
Config config.AppConfigurer
|
||||
OSCommand *oscommands.OSCommand
|
||||
Gui *gui.Gui
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
func Run(config config.AppConfigurer, common *common.Common, startArgs types.StartArgs) {
|
||||
func Run(
|
||||
config config.AppConfigurer,
|
||||
common *common.Common,
|
||||
startArgs appTypes.StartArgs,
|
||||
) {
|
||||
app, err := NewApp(config, common)
|
||||
|
||||
if err == nil {
|
||||
@@ -85,8 +86,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
|
||||
app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log))
|
||||
|
||||
var err error
|
||||
app.Updater, err = updates.NewUpdater(common, config, app.OSCommand)
|
||||
updater, err := updates.NewUpdater(common, config, app.OSCommand)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -96,66 +96,56 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) {
|
||||
return app, err
|
||||
}
|
||||
|
||||
gitVersion, err := app.validateGitVersion()
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
showRecentRepos, err := app.setupRepo()
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
gitConfig := git_config.NewStdCachedGitConfig(app.Log)
|
||||
|
||||
app.Gui, err = gui.NewGui(common, config, gitConfig, app.Updater, showRecentRepos, dirName)
|
||||
app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *App) validateGitVersion() error {
|
||||
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
|
||||
func (app *App) validateGitVersion() (*git_commands.GitVersion, error) {
|
||||
version, err := git_commands.GetGitVersion(app.OSCommand)
|
||||
// if we get an error anywhere here we'll show the same status
|
||||
minVersionError := errors.New(app.Tr.MinGitVersionError)
|
||||
if err != nil {
|
||||
return minVersionError
|
||||
return nil, minVersionError
|
||||
}
|
||||
|
||||
if isGitVersionValid(output) {
|
||||
return nil
|
||||
if version.IsOlderThan(2, 20, 0) {
|
||||
return nil, minVersionError
|
||||
}
|
||||
|
||||
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
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func isDirectoryAGitRepository(dir string) (bool, error) {
|
||||
info, err := os.Stat(filepath.Join(dir, ".git"))
|
||||
return info != nil && info.IsDir(), err
|
||||
return info != nil, err
|
||||
}
|
||||
|
||||
func openRecentRepo(app *App) bool {
|
||||
for _, repoDir := range app.Config.GetAppState().RecentRepos {
|
||||
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
|
||||
if err := os.Chdir(repoDir); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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 Git object
|
||||
return false, nil
|
||||
@@ -167,53 +157,85 @@ func (app *App) setupRepo() (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if isRepo, err := isDirectoryAGitRepository(cwd); isRepo {
|
||||
return false, err
|
||||
}
|
||||
|
||||
shouldInitRepo := true
|
||||
notARepository := app.UserConfig.NotARepository
|
||||
initialBranch := ""
|
||||
if notARepository == "prompt" {
|
||||
var shouldInitRepo bool
|
||||
initialBranchArg := ""
|
||||
switch app.UserConfig.NotARepository {
|
||||
case "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, " \r\n") != "y" {
|
||||
shouldInitRepo = false
|
||||
} else {
|
||||
shouldInitRepo = (strings.Trim(response, " \r\n") == "y")
|
||||
if shouldInitRepo {
|
||||
// Ask for the initial branch name
|
||||
fmt.Print(app.Tr.InitialBranch)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if trimmedResponse := strings.Trim(response, " \r\n"); len(trimmedResponse) > 0 {
|
||||
initialBranch += "--initial-branch=" + trimmedResponse
|
||||
initialBranchArg += "--initial-branch=" + app.OSCommand.Quote(trimmedResponse)
|
||||
}
|
||||
}
|
||||
} else if notARepository == "skip" {
|
||||
case "create":
|
||||
shouldInitRepo = true
|
||||
case "skip":
|
||||
shouldInitRepo = false
|
||||
}
|
||||
|
||||
if !shouldInitRepo {
|
||||
// check if we have a recent repo we can open
|
||||
for _, repoDir := range app.Config.GetAppState().RecentRepos {
|
||||
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
|
||||
if err := os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println(app.Tr.NoRecentRepositories)
|
||||
case "quit":
|
||||
fmt.Fprintln(os.Stderr, app.Tr.NotARepository)
|
||||
os.Exit(1)
|
||||
default:
|
||||
fmt.Fprintln(os.Stderr, app.Tr.IncorrectNotARepository)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranch).Run(); err != nil {
|
||||
|
||||
if shouldInitRepo {
|
||||
if err := app.OSCommand.Cmd.New("git init " + initialBranchArg).Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if we have a recent repo we can open
|
||||
for _, repoDir := range app.Config.GetAppState().RecentRepos {
|
||||
if isRepo, _ := isDirectoryAGitRepository(repoDir); isRepo {
|
||||
if err := os.Chdir(repoDir); err == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, app.Tr.NoRecentRepositories)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run this afterward so that the previous repo creation steps can run without this interfering
|
||||
if isBare, err := git_commands.IsBareRepo(app.OSCommand); isBare {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fmt.Print(app.Tr.BareRepo)
|
||||
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
|
||||
if shouldOpenRecent := strings.Trim(response, " \r\n") == "y"; !shouldOpenRecent {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if didOpenRepo := openRecentRepo(app); didOpenRepo {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
fmt.Println(app.Tr.NoRecentRepositories)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (app *App) Run(startArgs types.StartArgs) error {
|
||||
func (app *App) Run(startArgs appTypes.StartArgs) error {
|
||||
err := app.Gui.RunAndHandleError(startArgs)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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 {
|
||||
s := s
|
||||
t.Run(s.versionStr, func(t *testing.T) {
|
||||
result := isGitVersionValid(s.versionStr)
|
||||
assert.Equal(t, result, s.expectedResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
+252
-53
@@ -1,14 +1,17 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Sometimes lazygit will be invoked in daemon mode from a parent lazygit process.
|
||||
@@ -16,33 +19,58 @@ import (
|
||||
// For example, if we want to ensure that a git command doesn't hang due to
|
||||
// waiting for an editor to save a commit message, we can tell git to invoke lazygit
|
||||
// as the editor via 'GIT_EDITOR=lazygit', and use the env var
|
||||
// 'LAZYGIT_DAEMON_KIND=EXIT_IMMEDIATELY' to specify that we want to run lazygit
|
||||
// as a daemon which simply exits immediately. Any additional arguments we want
|
||||
// to pass to a daemon can be done via other env vars.
|
||||
// 'LAZYGIT_DAEMON_KIND=1' (exit immediately) to specify that we want to run lazygit
|
||||
// as a daemon which simply exits immediately.
|
||||
//
|
||||
// 'Daemon' is not the best name for this, because it's not a persistent background
|
||||
// process, but it's close enough.
|
||||
|
||||
type DaemonKind string
|
||||
type DaemonKind int
|
||||
|
||||
const (
|
||||
InteractiveRebase DaemonKind = "INTERACTIVE_REBASE"
|
||||
ExitImmediately DaemonKind = "EXIT_IMMEDIATELY"
|
||||
// for when we fail to parse the daemon kind
|
||||
DaemonKindUnknown DaemonKind = iota
|
||||
|
||||
DaemonKindExitImmediately
|
||||
DaemonKindCherryPick
|
||||
DaemonKindMoveTodoUp
|
||||
DaemonKindMoveTodoDown
|
||||
DaemonKindInsertBreak
|
||||
DaemonKindChangeTodoActions
|
||||
DaemonKindMoveFixupCommitDown
|
||||
)
|
||||
|
||||
const (
|
||||
DaemonKindEnvKey string = "LAZYGIT_DAEMON_KIND"
|
||||
RebaseTODOEnvKey string = "LAZYGIT_REBASE_TODO"
|
||||
|
||||
// Contains json-encoded arguments to the daemon
|
||||
DaemonInstructionEnvKey string = "LAZYGIT_DAEMON_INSTRUCTION"
|
||||
)
|
||||
|
||||
type Daemon interface {
|
||||
Run() error
|
||||
func getInstruction() Instruction {
|
||||
jsonData := os.Getenv(DaemonInstructionEnvKey)
|
||||
|
||||
mapping := map[DaemonKind]func(string) Instruction{
|
||||
DaemonKindExitImmediately: deserializeInstruction[*ExitImmediatelyInstruction],
|
||||
DaemonKindCherryPick: deserializeInstruction[*CherryPickCommitsInstruction],
|
||||
DaemonKindChangeTodoActions: deserializeInstruction[*ChangeTodoActionsInstruction],
|
||||
DaemonKindMoveFixupCommitDown: deserializeInstruction[*MoveFixupCommitDownInstruction],
|
||||
DaemonKindMoveTodoUp: deserializeInstruction[*MoveTodoUpInstruction],
|
||||
DaemonKindMoveTodoDown: deserializeInstruction[*MoveTodoDownInstruction],
|
||||
DaemonKindInsertBreak: deserializeInstruction[*InsertBreakInstruction],
|
||||
}
|
||||
|
||||
return mapping[getDaemonKind()](jsonData)
|
||||
}
|
||||
|
||||
func Handle(common *common.Common) {
|
||||
d := getDaemon(common)
|
||||
if d == nil {
|
||||
if !InDaemonMode() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := d.Run(); err != nil {
|
||||
instruction := getInstruction()
|
||||
|
||||
if err := instruction.run(common); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -50,58 +78,229 @@ func Handle(common *common.Common) {
|
||||
}
|
||||
|
||||
func InDaemonMode() bool {
|
||||
return getDaemonKind() != ""
|
||||
}
|
||||
|
||||
func getDaemon(common *common.Common) Daemon {
|
||||
switch getDaemonKind() {
|
||||
case InteractiveRebase:
|
||||
return &rebaseDaemon{c: common}
|
||||
case ExitImmediately:
|
||||
return &exitImmediatelyDaemon{c: common}
|
||||
}
|
||||
|
||||
return nil
|
||||
return getDaemonKind() != DaemonKindUnknown
|
||||
}
|
||||
|
||||
func getDaemonKind() DaemonKind {
|
||||
return DaemonKind(os.Getenv(DaemonKindEnvKey))
|
||||
intValue, err := strconv.Atoi(os.Getenv(DaemonKindEnvKey))
|
||||
if err != nil {
|
||||
return DaemonKindUnknown
|
||||
}
|
||||
|
||||
return DaemonKind(intValue)
|
||||
}
|
||||
|
||||
type rebaseDaemon struct {
|
||||
c *common.Common
|
||||
// An Instruction is a command to be run by lazygit in daemon mode.
|
||||
// It is serialized to json and passed to lazygit via environment variables
|
||||
type Instruction interface {
|
||||
Kind() DaemonKind
|
||||
SerializedInstructions() string
|
||||
|
||||
// runs the instruction
|
||||
run(common *common.Common) error
|
||||
}
|
||||
|
||||
func (self *rebaseDaemon) Run() error {
|
||||
self.c.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
self.c.Log.Info("args: ", os.Args)
|
||||
func serializeInstruction[T any](instruction T) string {
|
||||
jsonData, err := json.Marshal(instruction)
|
||||
if err != nil {
|
||||
// this should never happen
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
|
||||
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv(RebaseTODOEnvKey)), 0o644); err != nil {
|
||||
return err
|
||||
return string(jsonData)
|
||||
}
|
||||
|
||||
func deserializeInstruction[T Instruction](jsonData string) Instruction {
|
||||
var instruction T
|
||||
err := json.Unmarshal([]byte(jsonData), &instruction)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return instruction
|
||||
}
|
||||
|
||||
func ToEnvVars(instruction Instruction) []string {
|
||||
return []string{
|
||||
fmt.Sprintf("%s=%d", DaemonKindEnvKey, instruction.Kind()),
|
||||
fmt.Sprintf("%s=%s", DaemonInstructionEnvKey, instruction.SerializedInstructions()),
|
||||
}
|
||||
}
|
||||
|
||||
type ExitImmediatelyInstruction struct{}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) Kind() DaemonKind {
|
||||
return DaemonKindExitImmediately
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ExitImmediatelyInstruction) run(common *common.Common) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewExitImmediatelyInstruction() Instruction {
|
||||
return &ExitImmediatelyInstruction{}
|
||||
}
|
||||
|
||||
type CherryPickCommitsInstruction struct {
|
||||
Todo string
|
||||
}
|
||||
|
||||
func NewCherryPickCommitsInstruction(commits []*models.Commit) Instruction {
|
||||
todoLines := lo.Map(commits, func(commit *models.Commit, _ int) TodoLine {
|
||||
return TodoLine{
|
||||
Action: "pick",
|
||||
Commit: commit,
|
||||
}
|
||||
} 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 {
|
||||
self.c.Log.Info("Lazygit demon did not match on any use cases")
|
||||
})
|
||||
|
||||
todo := TodoLinesToString(todoLines)
|
||||
|
||||
return &CherryPickCommitsInstruction{
|
||||
Todo: todo,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
func (self *CherryPickCommitsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindCherryPick
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *CherryPickCommitsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte(self.Todo))
|
||||
})
|
||||
}
|
||||
|
||||
type ChangeTodoActionsInstruction struct {
|
||||
Changes []ChangeTodoAction
|
||||
}
|
||||
|
||||
func NewChangeTodoActionsInstruction(changes []ChangeTodoAction) Instruction {
|
||||
return &ChangeTodoActionsInstruction{
|
||||
Changes: changes,
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
type exitImmediatelyDaemon struct {
|
||||
c *common.Common
|
||||
func (self *ChangeTodoActionsInstruction) Kind() DaemonKind {
|
||||
return DaemonKindChangeTodoActions
|
||||
}
|
||||
|
||||
func (self *exitImmediatelyDaemon) Run() error {
|
||||
return nil
|
||||
func (self *ChangeTodoActionsInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
for _, c := range self.Changes {
|
||||
if err := utils.EditRebaseTodo(path, c.Sha, todo.Pick, c.NewAction); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Takes the sha of some commit, and the sha of a fixup commit that was created
|
||||
// at the end of the branch, then moves the fixup commit down to right after the
|
||||
// original commit, changing its type to "fixup"
|
||||
type MoveFixupCommitDownInstruction struct {
|
||||
OriginalSha string
|
||||
FixupSha string
|
||||
}
|
||||
|
||||
func NewMoveFixupCommitDownInstruction(originalSha string, fixupSha string) Instruction {
|
||||
return &MoveFixupCommitDownInstruction{
|
||||
OriginalSha: originalSha,
|
||||
FixupSha: fixupSha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveFixupCommitDown
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveFixupCommitDownInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveFixupCommitDown(path, self.OriginalSha, self.FixupSha)
|
||||
})
|
||||
}
|
||||
|
||||
type MoveTodoUpInstruction struct {
|
||||
Sha string
|
||||
}
|
||||
|
||||
func NewMoveTodoUpInstruction(sha string) Instruction {
|
||||
return &MoveTodoUpInstruction{
|
||||
Sha: sha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveTodoUp
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodoUpInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveTodoUp(path, self.Sha, todo.Pick)
|
||||
})
|
||||
}
|
||||
|
||||
type MoveTodoDownInstruction struct {
|
||||
Sha string
|
||||
}
|
||||
|
||||
func NewMoveTodoDownInstruction(sha string) Instruction {
|
||||
return &MoveTodoDownInstruction{
|
||||
Sha: sha,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) Kind() DaemonKind {
|
||||
return DaemonKindMoveTodoDown
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *MoveTodoDownInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.MoveTodoDown(path, self.Sha, todo.Pick)
|
||||
})
|
||||
}
|
||||
|
||||
type InsertBreakInstruction struct{}
|
||||
|
||||
func NewInsertBreakInstruction() Instruction {
|
||||
return &InsertBreakInstruction{}
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) Kind() DaemonKind {
|
||||
return DaemonKindInsertBreak
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) SerializedInstructions() string {
|
||||
return serializeInstruction(self)
|
||||
}
|
||||
|
||||
func (self *InsertBreakInstruction) run(common *common.Common) error {
|
||||
return handleInteractiveRebase(common, func(path string) error {
|
||||
return utils.PrependStrToTodoFile(path, []byte("break\n"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
)
|
||||
|
||||
type TodoLine struct {
|
||||
Action string
|
||||
Commit *models.Commit
|
||||
}
|
||||
|
||||
func (self *TodoLine) ToString() string {
|
||||
if self.Action == "break" {
|
||||
return self.Action + "\n"
|
||||
} else {
|
||||
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
func TodoLinesToString(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
})
|
||||
|
||||
return strings.Join(slices.Reverse(lines), "")
|
||||
}
|
||||
|
||||
type ChangeTodoAction struct {
|
||||
Sha string
|
||||
NewAction todo.TodoCommand
|
||||
}
|
||||
|
||||
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
|
||||
common.Log.Info("Lazygit invoked as interactive rebase demon")
|
||||
common.Log.Info("args: ", os.Args)
|
||||
path := os.Args[1]
|
||||
|
||||
if strings.HasSuffix(path, "git-rebase-todo") {
|
||||
return f(path)
|
||||
} else if strings.HasSuffix(path, 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 {
|
||||
common.Log.Info("Lazygit demon did not match on any use cases")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func gitDir() string {
|
||||
dir := env.GetGitDirEnv()
|
||||
if dir == "" {
|
||||
return ".git"
|
||||
}
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type cliArgs struct {
|
||||
RepoPath string
|
||||
FilterPath string
|
||||
GitArg string
|
||||
PrintVersionInfo bool
|
||||
Debug bool
|
||||
TailLogs bool
|
||||
PrintDefaultConfig bool
|
||||
PrintConfigDir bool
|
||||
UseConfigDir string
|
||||
WorkTree string
|
||||
GitDir string
|
||||
CustomConfigFile string
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
Commit string
|
||||
Date string
|
||||
Version string
|
||||
BuildSource string
|
||||
}
|
||||
|
||||
func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
|
||||
cliArgs := parseCliArgsAndEnvVars()
|
||||
mergeBuildInfo(buildInfo)
|
||||
|
||||
if cliArgs.RepoPath != "" {
|
||||
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(cliArgs.RepoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if isRepo, err := isDirectoryAGitRepository(absRepoPath); err != nil || !isRepo {
|
||||
log.Fatal(absRepoPath + " is not a valid git repository.")
|
||||
}
|
||||
|
||||
cliArgs.WorkTree = absRepoPath
|
||||
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
|
||||
}
|
||||
|
||||
if cliArgs.CustomConfigFile != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", cliArgs.CustomConfigFile)
|
||||
}
|
||||
|
||||
if cliArgs.UseConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
env.SetGitWorkTreeEnv(cliArgs.WorkTree)
|
||||
}
|
||||
|
||||
if cliArgs.GitDir != "" {
|
||||
env.SetGitDirEnv(cliArgs.GitDir)
|
||||
}
|
||||
|
||||
if cliArgs.PrintVersionInfo {
|
||||
gitVersion := getGitVersionInfo()
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s, git version=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH, gitVersion)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.PrintDefaultConfig {
|
||||
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 cliArgs.PrintConfigDir {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.TailLogs {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
if err := os.Chdir(cliArgs.WorkTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", buildInfo.Version, buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, cliArgs.Debug, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if integrationTest != nil {
|
||||
integrationTest.SetupConfig(appConfig)
|
||||
}
|
||||
|
||||
common, err := NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(cliArgs.GitArg)
|
||||
|
||||
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
|
||||
}
|
||||
|
||||
func parseCliArgsAndEnvVars() *cliArgs {
|
||||
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")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
printVersionInfo := false
|
||||
flaggy.Bool(&printVersionInfo, "v", "version", "Print the current version")
|
||||
|
||||
debug := false
|
||||
flaggy.Bool(&debug, "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)")
|
||||
|
||||
tailLogs := false
|
||||
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
printDefaultConfig := false
|
||||
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
|
||||
|
||||
printConfigDir := false
|
||||
flaggy.Bool(&printConfigDir, "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")
|
||||
|
||||
customConfigFile := ""
|
||||
flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debug = true
|
||||
}
|
||||
|
||||
return &cliArgs{
|
||||
RepoPath: repoPath,
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
PrintVersionInfo: printVersionInfo,
|
||||
Debug: debug,
|
||||
TailLogs: tailLogs,
|
||||
PrintDefaultConfig: printDefaultConfig,
|
||||
PrintConfigDir: printConfigDir,
|
||||
UseConfigDir: useConfigDir,
|
||||
WorkTree: workTree,
|
||||
GitDir: gitDir,
|
||||
CustomConfigFile: customConfigFile,
|
||||
}
|
||||
}
|
||||
|
||||
func parseGitArg(gitArg string) appTypes.GitArg {
|
||||
typedArg := appTypes.GitArg(gitArg)
|
||||
|
||||
// using switch so that linter catches when a new git arg value is defined but not handled here
|
||||
switch typedArg {
|
||||
case appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
string(appTypes.GitArgStatus),
|
||||
string(appTypes.GitArgBranch),
|
||||
string(appTypes.GitArgLog),
|
||||
string(appTypes.GitArgStash),
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// the buildInfo struct we get passed in is based on what's baked into the lazygit
|
||||
// binary via the LDFLAGS argument. Some lazygit distributions will make use of these
|
||||
// arguments and some will not. Go recently started baking in build info
|
||||
// into the binary by default e.g. the git commit hash. So in this function
|
||||
// we merge the two together, giving priority to the stuff set by LDFLAGS.
|
||||
// Note: this mutates the argument passed in
|
||||
func mergeBuildInfo(buildInfo *BuildInfo) {
|
||||
// if the version has already been set by build flags then we'll honour that.
|
||||
// chances are it's something like v0.31.0 which is more informative than a
|
||||
// commit hash.
|
||||
if buildInfo.Version != "" {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo.Version = "unversioned"
|
||||
|
||||
goBuildInfo, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.revision"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Commit = revision.Value
|
||||
// if lazygit was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
buildInfo.Version = utils.ShortSha(revision.Value)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Date = time.Value
|
||||
}
|
||||
}
|
||||
|
||||
func getGitVersionInfo() string {
|
||||
cmd := secureexec.Command("git", "--version")
|
||||
stdout, _ := cmd.Output()
|
||||
gitVersion := strings.Trim(strings.TrimPrefix(string(stdout), "git version "), " \r\n")
|
||||
return gitVersion
|
||||
}
|
||||
+4
-9
@@ -1,7 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
|
||||
if config.GetDebug() {
|
||||
log = newDevelopmentLogger()
|
||||
} else {
|
||||
log = newProductionLogger()
|
||||
@@ -21,17 +21,12 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
return log.WithFields(logrus.Fields{
|
||||
"debug": config.GetDebug(),
|
||||
"version": config.GetVersion(),
|
||||
"commit": config.GetCommit(),
|
||||
"buildDate": config.GetBuildDate(),
|
||||
})
|
||||
return log.WithFields(logrus.Fields{})
|
||||
}
|
||||
|
||||
func newProductionLogger() *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
log.Out = io.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// StartArgs is the struct that represents some things we want to do on program start
|
||||
type StartArgs struct {
|
||||
// FilterPath determines which path we're going to filter on so that we only see commits from that file.
|
||||
FilterPath string
|
||||
// GitArg determines what context we open in
|
||||
GitArg GitArg
|
||||
// integration test (only relevant when invoking lazygit in the context of an integration test)
|
||||
IntegrationTest integrationTypes.IntegrationTest
|
||||
}
|
||||
|
||||
type GitArg string
|
||||
|
||||
const (
|
||||
GitArgNone GitArg = ""
|
||||
GitArgStatus GitArg = "status"
|
||||
GitArgBranch GitArg = "branch"
|
||||
GitArgLog GitArg = "log"
|
||||
GitArgStash GitArg = "stash"
|
||||
)
|
||||
|
||||
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
|
||||
return StartArgs{
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
IntegrationTest: test,
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package cheatsheet
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func Check() {
|
||||
dir := GetDir()
|
||||
dir := GetKeybindingsDir()
|
||||
tmpDir := filepath.Join(os.TempDir(), "lazygit_cheatsheet")
|
||||
err := os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
@@ -60,7 +59,7 @@ func obtainContent(dir string) string {
|
||||
content := ""
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if re.MatchString(path) {
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
bytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("Error occurred while checking if cheatsheets are up to date: %v", err)
|
||||
}
|
||||
|
||||
+47
-51
@@ -15,13 +15,12 @@ import (
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazycore/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
@@ -45,8 +44,8 @@ func CommandToRun() string {
|
||||
return "go run scripts/cheatsheet/main.go generate"
|
||||
}
|
||||
|
||||
func GetDir() string {
|
||||
return integration.GetRootDirectory() + "/docs/keybindings"
|
||||
func GetKeybindingsDir() string {
|
||||
return utils.GetLazyRootDirectory() + "/docs/keybindings"
|
||||
}
|
||||
|
||||
func generateAtDir(cheatsheetDir string) {
|
||||
@@ -76,7 +75,7 @@ func generateAtDir(cheatsheetDir string) {
|
||||
}
|
||||
|
||||
func Generate() {
|
||||
generateAtDir(GetDir())
|
||||
generateAtDir(GetKeybindingsDir())
|
||||
}
|
||||
|
||||
func writeString(file *os.File, str string) {
|
||||
@@ -88,34 +87,34 @@ func writeString(file *os.File, str string) {
|
||||
|
||||
func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
contextTitleMap := map[string]string{
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.MainTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"merging": tr.MergingTitle,
|
||||
"normal": tr.NormalTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
"global": tr.GlobalTitle,
|
||||
"navigation": tr.NavigationTitle,
|
||||
"branches": tr.BranchesTitle,
|
||||
"localBranches": tr.LocalBranchesTitle,
|
||||
"files": tr.FilesTitle,
|
||||
"status": tr.StatusTitle,
|
||||
"submodules": tr.SubmodulesTitle,
|
||||
"subCommits": tr.SubCommitsTitle,
|
||||
"remoteBranches": tr.RemoteBranchesTitle,
|
||||
"remotes": tr.RemotesTitle,
|
||||
"reflogCommits": tr.ReflogCommitsTitle,
|
||||
"tags": tr.TagsTitle,
|
||||
"commitFiles": tr.CommitFilesTitle,
|
||||
"commitMessage": tr.CommitMessageTitle,
|
||||
"commitDescription": tr.CommitDescriptionTitle,
|
||||
"commits": tr.CommitsTitle,
|
||||
"confirmation": tr.ConfirmationTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
"stash": tr.StashTitle,
|
||||
"suggestions": tr.SuggestionsCheatsheetTitle,
|
||||
"extras": tr.ExtrasTitle,
|
||||
}
|
||||
|
||||
title, ok := contextTitleMap[str]
|
||||
@@ -127,19 +126,24 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
}
|
||||
|
||||
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
|
||||
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
|
||||
bindingsToDisplay := slices.Filter(bindings, func(binding *types.Binding) bool {
|
||||
return binding.Description != "" || binding.Alternative != ""
|
||||
if lo.Contains(excludedViews, binding.ViewName) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (binding.Description != "" || binding.Alternative != "") && binding.Key != nil
|
||||
})
|
||||
|
||||
bindingsByHeader := utils.MuiltiGroupBy(bindingsToDisplay, func(binding *types.Binding) []header {
|
||||
return getHeaders(binding, tr)
|
||||
bindingsByHeader := lo.GroupBy(bindingsToDisplay, func(binding *types.Binding) header {
|
||||
return getHeader(binding, tr)
|
||||
})
|
||||
|
||||
bindingGroups := maps.MapToSlice(
|
||||
bindingsByHeader,
|
||||
func(header header, hBindings []*types.Binding) headerWithBindings {
|
||||
uniqBindings := lo.UniqBy(hBindings, func(binding *types.Binding) string {
|
||||
return binding.Description + keybindings.GetKeyDisplay(binding.Key)
|
||||
return binding.Description + keybindings.LabelFromKey(binding.Key)
|
||||
})
|
||||
|
||||
return headerWithBindings{
|
||||
@@ -164,24 +168,16 @@ func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*b
|
||||
})
|
||||
}
|
||||
|
||||
// a binding may belong to multiple headers if it is applicable to multiple contexts,
|
||||
// for example the copy-to-clipboard binding.
|
||||
func getHeaders(binding *types.Binding, tr *i18n.TranslationSet) []header {
|
||||
func getHeader(binding *types.Binding, tr *i18n.TranslationSet) header {
|
||||
if binding.Tag == "navigation" {
|
||||
return []header{{priority: 2, title: localisedTitle(tr, "navigation")}}
|
||||
return header{priority: 2, title: localisedTitle(tr, "navigation")}
|
||||
}
|
||||
|
||||
if binding.ViewName == "" {
|
||||
return []header{{priority: 3, title: localisedTitle(tr, "global")}}
|
||||
return header{priority: 3, title: localisedTitle(tr, "global")}
|
||||
}
|
||||
|
||||
if len(binding.Contexts) == 0 {
|
||||
return []header{}
|
||||
}
|
||||
|
||||
return slices.Map(binding.Contexts, func(context string) header {
|
||||
return header{priority: 1, title: localisedTitle(tr, context)}
|
||||
})
|
||||
return header{priority: 1, title: localisedTitle(tr, binding.ViewName)}
|
||||
}
|
||||
|
||||
func formatSections(tr *i18n.TranslationSet, bindingSections []*bindingSection) string {
|
||||
@@ -207,10 +203,10 @@ func formatBinding(binding *types.Binding) string {
|
||||
if binding.Alternative != "" {
|
||||
return fmt.Sprintf(
|
||||
" <kbd>%s</kbd>: %s (%s)\n",
|
||||
keybindings.GetKeyDisplay(binding.Key),
|
||||
keybindings.LabelFromKey(binding.Key),
|
||||
binding.Description,
|
||||
binding.Alternative,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.GetKeyDisplay(binding.Key), binding.Description)
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", keybindings.LabelFromKey(binding.Key), binding.Description)
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
@@ -36,8 +36,8 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,6 +49,7 @@ func TestGetBindingSections(t *testing.T) {
|
||||
{
|
||||
ViewName: "",
|
||||
Description: "quit",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
@@ -58,6 +59,7 @@ func TestGetBindingSections(t *testing.T) {
|
||||
{
|
||||
ViewName: "",
|
||||
Description: "quit",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -69,17 +71,17 @@ func TestGetBindingSections(t *testing.T) {
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
Contexts: []string{"files"},
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
Contexts: []string{"files"},
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
ViewName: "submodules",
|
||||
Description: "drop submodule",
|
||||
Contexts: []string{"submodules"},
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
@@ -89,12 +91,12 @@ func TestGetBindingSections(t *testing.T) {
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "stage file",
|
||||
Contexts: []string{"files"},
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Description: "unstage file",
|
||||
Contexts: []string{"files"},
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -102,9 +104,9 @@ func TestGetBindingSections(t *testing.T) {
|
||||
title: "Submodules",
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
ViewName: "submodules",
|
||||
Description: "drop submodule",
|
||||
Contexts: []string{"submodules"},
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -115,24 +117,24 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "unstage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "scroll",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "revert commit",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
expected: []*bindingSection{
|
||||
@@ -141,8 +143,8 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "scroll",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
@@ -152,8 +154,8 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "revert commit",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -162,13 +164,13 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "unstage file",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -179,35 +181,35 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "unstage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "scroll",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "revert commit",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "scroll",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "page up",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
@@ -217,14 +219,14 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "scroll",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "page up",
|
||||
Key: 'a',
|
||||
Tag: "navigation",
|
||||
},
|
||||
},
|
||||
@@ -234,8 +236,8 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{"commits"},
|
||||
Description: "revert commit",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -244,13 +246,13 @@ func TestGetBindingSections(t *testing.T) {
|
||||
bindings: []*types.Binding{
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "stage file",
|
||||
Key: 'a',
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{"files"},
|
||||
Description: "unstage file",
|
||||
Key: 'a',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+42
-27
@@ -1,18 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
@@ -43,21 +41,22 @@ type GitCommand struct {
|
||||
}
|
||||
|
||||
type Loaders struct {
|
||||
Branches *loaders.BranchLoader
|
||||
CommitFiles *loaders.CommitFileLoader
|
||||
Commits *loaders.CommitLoader
|
||||
Files *loaders.FileLoader
|
||||
ReflogCommits *loaders.ReflogCommitLoader
|
||||
Remotes *loaders.RemoteLoader
|
||||
Stash *loaders.StashLoader
|
||||
Tags *loaders.TagLoader
|
||||
BranchLoader *git_commands.BranchLoader
|
||||
CommitFileLoader *git_commands.CommitFileLoader
|
||||
CommitLoader *git_commands.CommitLoader
|
||||
FileLoader *git_commands.FileLoader
|
||||
ReflogCommitLoader *git_commands.ReflogCommitLoader
|
||||
RemoteLoader *git_commands.RemoteLoader
|
||||
StashLoader *git_commands.StashLoader
|
||||
TagLoader *git_commands.TagLoader
|
||||
}
|
||||
|
||||
func NewGitCommand(
|
||||
cmn *common.Common,
|
||||
version *git_commands.GitVersion,
|
||||
osCommand *oscommands.OSCommand,
|
||||
gitConfig git_config.IGitConfig,
|
||||
syncMutex *sync.Mutex,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) (*GitCommand, error) {
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
@@ -68,13 +67,14 @@ func NewGitCommand(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
|
||||
dotGitDir, err := findDotGitDir(os.Stat, os.ReadFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewGitCommandAux(
|
||||
cmn,
|
||||
version,
|
||||
osCommand,
|
||||
gitConfig,
|
||||
dotGitDir,
|
||||
@@ -85,11 +85,12 @@ func NewGitCommand(
|
||||
|
||||
func NewGitCommandAux(
|
||||
cmn *common.Common,
|
||||
version *git_commands.GitVersion,
|
||||
osCommand *oscommands.OSCommand,
|
||||
gitConfig git_config.IGitConfig,
|
||||
dotGitDir string,
|
||||
repo *gogit.Repository,
|
||||
syncMutex *sync.Mutex,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) *GitCommand {
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
|
||||
|
||||
@@ -99,10 +100,11 @@ func NewGitCommandAux(
|
||||
// on the one struct.
|
||||
// common ones are: cmn, osCommand, dotGitDir, configCommands
|
||||
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
|
||||
gitCommon := git_commands.NewGitCommon(cmn, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
|
||||
|
||||
fileLoader := git_commands.NewFileLoader(cmn, cmd, configCommands)
|
||||
|
||||
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, dotGitDir, repo, configCommands, syncMutex)
|
||||
statusCommands := git_commands.NewStatusCommands(gitCommon)
|
||||
fileLoader := loaders.NewFileLoader(cmn, cmd, configCommands)
|
||||
flowCommands := git_commands.NewFlowCommands(gitCommon)
|
||||
remoteCommands := git_commands.NewRemoteCommands(gitCommon)
|
||||
branchCommands := git_commands.NewBranchCommands(gitCommon)
|
||||
@@ -115,11 +117,24 @@ func NewGitCommandAux(
|
||||
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
|
||||
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
|
||||
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
|
||||
// TODO: have patch manager take workingTreeCommands in its entirety
|
||||
patchManager := patch.NewPatchManager(cmn.Log, workingTreeCommands.ApplyPatch, workingTreeCommands.ShowFileDiff)
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchManager)
|
||||
// TODO: have patch builder take workingTreeCommands in its entirety
|
||||
patchBuilder := patch.NewPatchBuilder(cmn.Log, workingTreeCommands.ApplyPatch,
|
||||
func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
|
||||
// TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into
|
||||
// account. For now we just pass false.
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, plain, false)
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
branchLoader := git_commands.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchInfo, configCommands)
|
||||
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
|
||||
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode)
|
||||
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
|
||||
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
|
||||
stashLoader := git_commands.NewStashLoader(cmn, cmd)
|
||||
tagLoader := git_commands.NewTagLoader(cmn, cmd)
|
||||
|
||||
return &GitCommand{
|
||||
Branch: branchCommands,
|
||||
Commit: commitCommands,
|
||||
@@ -138,14 +153,14 @@ func NewGitCommandAux(
|
||||
Bisect: bisectCommands,
|
||||
WorkingTree: workingTreeCommands,
|
||||
Loaders: Loaders{
|
||||
Branches: loaders.NewBranchLoader(cmn, branchCommands.GetRawBranches, branchCommands.CurrentBranchName, configCommands),
|
||||
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
|
||||
Commits: loaders.NewCommitLoader(cmn, cmd, dotGitDir, branchCommands.CurrentBranchName, statusCommands.RebaseMode),
|
||||
Files: fileLoader,
|
||||
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
|
||||
Remotes: loaders.NewRemoteLoader(cmn, cmd, repo.Remotes),
|
||||
Stash: loaders.NewStashLoader(cmn, cmd),
|
||||
Tags: loaders.NewTagLoader(cmn, cmd),
|
||||
BranchLoader: branchLoader,
|
||||
CommitFileLoader: commitFileLoader,
|
||||
CommitLoader: commitLoader,
|
||||
FileLoader: fileLoader,
|
||||
ReflogCommitLoader: reflogCommitLoader,
|
||||
RemoteLoader: remoteLoader,
|
||||
StashLoader: stashLoader,
|
||||
TagLoader: tagLoader,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, e
|
||||
return self.innerRunner.RunWithOutput(cmdObj)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutputs(cmdObj oscommands.ICmdObj) (string, string, error) {
|
||||
return self.innerRunner.RunWithOutputs(cmdObj)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
|
||||
}
|
||||
|
||||
@@ -2,19 +2,12 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// this takes something like:
|
||||
// * (HEAD detached at 264fc6f5)
|
||||
// remotes
|
||||
// and returns '264fc6f5' as the second match
|
||||
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
|
||||
|
||||
type BranchCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -30,29 +23,38 @@ func (self *BranchCommands) New(name string, base string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run()
|
||||
}
|
||||
|
||||
// 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 (self *BranchCommands) CurrentBranchName() (string, string, error) {
|
||||
// CurrentBranchInfo get the current branch information.
|
||||
func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
|
||||
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput()
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
return BranchInfo{
|
||||
RefName: trimmedBranchName,
|
||||
DisplayName: trimmedBranchName,
|
||||
DetachedHead: false,
|
||||
}, nil
|
||||
}
|
||||
output, err := self.cmd.New("git branch --contains").DontLog().RunWithOutput()
|
||||
output, err := self.cmd.New(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return BranchInfo{}, 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
|
||||
split := strings.Split(strings.TrimRight(line, "\r\n"), "\x00")
|
||||
if len(split) == 3 && split[0] == "*" {
|
||||
sha := split[1]
|
||||
displayName := split[2]
|
||||
return BranchInfo{
|
||||
RefName: sha,
|
||||
DisplayName: displayName,
|
||||
DetachedHead: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
return "HEAD", "HEAD", nil
|
||||
return BranchInfo{
|
||||
RefName: "HEAD",
|
||||
DisplayName: "HEAD",
|
||||
DetachedHead: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete delete branch
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/go-git/v5/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
type BranchLoaderConfigCommands interface {
|
||||
Branches() (map[string]*config.Branch, error)
|
||||
}
|
||||
|
||||
type BranchInfo struct {
|
||||
RefName string
|
||||
DisplayName string // e.g. '(HEAD detached at 123asdf)'
|
||||
DetachedHead bool
|
||||
}
|
||||
|
||||
// BranchLoader returns a list of Branch objects for the current repo
|
||||
type BranchLoader struct {
|
||||
*common.Common
|
||||
getRawBranches func() (string, error)
|
||||
getCurrentBranchInfo func() (BranchInfo, error)
|
||||
config BranchLoaderConfigCommands
|
||||
}
|
||||
|
||||
func NewBranchLoader(
|
||||
cmn *common.Common,
|
||||
getRawBranches func() (string, error),
|
||||
getCurrentBranchInfo func() (BranchInfo, error),
|
||||
config BranchLoaderConfigCommands,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
Common: cmn,
|
||||
getRawBranches: getRawBranches,
|
||||
getCurrentBranchInfo: getCurrentBranchInfo,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Load the list of branches for the current repo
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
|
||||
branches := self.obtainBranches()
|
||||
|
||||
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
||||
|
||||
// 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 = slices.Remove(branches, j)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = slices.Prepend(branches, branchesWithRecency...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = slices.Move(branches, i, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
info, err := self.getCurrentBranchInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branches = slices.Prepend(branches, &models.Branch{Name: info.RefName, DisplayName: info.DisplayName, Head: true, DetachedHead: info.DetachedHead, Recency: " *"})
|
||||
}
|
||||
|
||||
configBranches, err := self.config.Branches()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
match := configBranches[branch.Name]
|
||||
if match != nil {
|
||||
branch.UpstreamRemote = match.Remote
|
||||
branch.UpstreamBranch = match.Merge.Short()
|
||||
}
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
output, err := self.getRawBranches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
|
||||
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
|
||||
if line == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
split := strings.Split(line, "\x00")
|
||||
if len(split) != 4 {
|
||||
// Ignore line if it isn't separated into 4 parts
|
||||
// This is probably a warning message, for more info see:
|
||||
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return obtainBranch(split), true
|
||||
})
|
||||
}
|
||||
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
// split contains the '|' separated tokens in the line of output
|
||||
func obtainBranch(split []string) *models.Branch {
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
// if we're here then it means we do not have a local version of the remote.
|
||||
// The branch might still be tracking a remote though, we just don't know
|
||||
// how many commits ahead/behind it is
|
||||
return branch
|
||||
}
|
||||
|
||||
track := split[3]
|
||||
if track == "[gone]" {
|
||||
branch.UpstreamGone = true
|
||||
} else {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
return branch
|
||||
}
|
||||
|
||||
// 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 (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
|
||||
foundBranches := set.New[string]()
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
|
||||
|
||||
for _, commit := range reflogCommits {
|
||||
match := re.FindStringSubmatch(commit.Name)
|
||||
if len(match) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranches.Includes(branchName) {
|
||||
foundBranches.Add(branchName)
|
||||
reflogBranches = append(reflogBranches, &models.Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflogBranches
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package git_commands
|
||||
|
||||
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObtainBanch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input []string
|
||||
expectedBranch *models.Branch
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "TrimHeads",
|
||||
input: []string{"", "heads/a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "NoUpstream",
|
||||
input: []string{"", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "IsHead",
|
||||
input: []string{"*", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
|
||||
},
|
||||
{
|
||||
testName: "IsBehindAndAhead",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "RemoteBranchIsGone",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branch := obtainBranch(s.input)
|
||||
assert.EqualValues(t, s.expectedBranch, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -53,10 +54,10 @@ func TestBranchGetCommitDifferences(t *testing.T) {
|
||||
|
||||
func TestBranchNewBranch(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -b "test" "master"`, "", nil)
|
||||
Expect(`git checkout -b "test" "refs/heads/master"`, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.New("test", "master"))
|
||||
assert.NoError(t, instance.New("test", "refs/heads/master"))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
@@ -99,12 +100,53 @@ func TestBranchDeleteBranch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBranchMerge(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge --no-edit "test"`, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner})
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
userConfig *config.UserConfig
|
||||
opts MergeOpts
|
||||
branchName string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "basic",
|
||||
userConfig: &config.UserConfig{},
|
||||
opts: MergeOpts{},
|
||||
branchName: "mybranch",
|
||||
expected: `git merge --no-edit "mybranch"`,
|
||||
},
|
||||
{
|
||||
testName: "merging args",
|
||||
userConfig: &config.UserConfig{
|
||||
Git: config.GitConfig{
|
||||
Merging: config.MergingConfig{
|
||||
Args: "--merging-args", // it's up to the user what they put here
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: MergeOpts{},
|
||||
branchName: "mybranch",
|
||||
expected: `git merge --no-edit --merging-args "mybranch"`,
|
||||
},
|
||||
{
|
||||
testName: "fast forward only",
|
||||
userConfig: &config.UserConfig{},
|
||||
opts: MergeOpts{FastForwardOnly: true},
|
||||
branchName: "mybranch",
|
||||
expected: `git merge --no-edit --ff-only "mybranch"`,
|
||||
},
|
||||
}
|
||||
|
||||
assert.NoError(t, instance.Merge("test", MergeOpts{}))
|
||||
runner.CheckForMissingCalls()
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(s.expected, "", nil)
|
||||
instance := buildBranchCommands(commonDeps{runner: runner, userConfig: s.userConfig})
|
||||
|
||||
assert.NoError(t, instance.Merge(s.branchName, s.opts))
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBranchCheckout(t *testing.T) {
|
||||
@@ -162,54 +204,62 @@ func TestBranchGetAllBranchGraph(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBranchCurrentBranchName(t *testing.T) {
|
||||
func TestBranchCurrentBranchInfo(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(string, string, error)
|
||||
test func(BranchInfo, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"says we are on the master branch if we are",
|
||||
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
func(info BranchInfo, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
assert.EqualValues(t, "master", info.RefName)
|
||||
assert.EqualValues(t, "master", info.DisplayName)
|
||||
assert.False(t, info.DetachedHead)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falls back to git `git branch --contains` if symbolic-ref fails",
|
||||
"falls back to git `git branch --points-at=HEAD` if symbolic-ref fails",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "* master", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "*\x006f71c57a8d4bd6c11399c3f55f42c815527a73a4\x00(HEAD detached at 6f71c57a)\n", nil),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
assert.EqualValues(t, "6f71c57a8d4bd6c11399c3f55f42c815527a73a4", info.RefName)
|
||||
assert.EqualValues(t, "(HEAD detached at 6f71c57a)", info.DisplayName)
|
||||
assert.True(t, info.DetachedHead)
|
||||
},
|
||||
},
|
||||
{
|
||||
"handles a detached head",
|
||||
"handles a detached head (LANG=zh_CN.UTF-8)",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
Expect(
|
||||
`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`,
|
||||
"*\x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00(头指针在 679b0456 分离)\n"+
|
||||
" \x00679b0456f3db7c505b398def84e7d023e5b55a8d\x00refs/heads/master\n",
|
||||
nil),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123abcd", name)
|
||||
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
|
||||
assert.EqualValues(t, "679b0456f3db7c505b398def84e7d023e5b55a8d", info.RefName)
|
||||
assert.EqualValues(t, "(头指针在 679b0456 分离)", info.DisplayName)
|
||||
assert.True(t, info.DetachedHead)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "", errors.New("error")),
|
||||
func(name string, displayname string, err error) {
|
||||
Expect(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`, "", errors.New("error")),
|
||||
func(info BranchInfo, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", name)
|
||||
assert.EqualValues(t, "", displayname)
|
||||
assert.EqualValues(t, "", info.RefName)
|
||||
assert.EqualValues(t, "", info.DisplayName)
|
||||
assert.False(t, info.DetachedHead)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -218,7 +268,7 @@ func TestBranchCurrentBranchName(t *testing.T) {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildBranchCommands(commonDeps{runner: s.runner})
|
||||
s.test(instance.CurrentBranchName())
|
||||
s.test(instance.CurrentBranchInfo())
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
var ErrInvalidCommitIndex = errors.New("invalid commit index")
|
||||
|
||||
type CommitCommands struct {
|
||||
*GitCommon
|
||||
}
|
||||
@@ -18,11 +20,6 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
|
||||
}
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
return self.cmd.New("git commit --allow-empty --amend --only -m " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
// ResetAuthor resets the author of the topmost commit
|
||||
func (self *CommitCommands) ResetAuthor() error {
|
||||
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run()
|
||||
@@ -45,11 +42,7 @@ func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars [
|
||||
}
|
||||
|
||||
func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", self.cmd.Quote(line))
|
||||
}
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
|
||||
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
|
||||
noVerifyFlag := ""
|
||||
@@ -57,12 +50,28 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
|
||||
noVerifyFlag = " --no-verify"
|
||||
}
|
||||
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), lineArgs))
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs))
|
||||
}
|
||||
|
||||
// RewordLastCommit rewords the topmost commit with the given message
|
||||
func (self *CommitCommands) RewordLastCommit(message string) error {
|
||||
messageArgs := self.commitMessageArgs(message)
|
||||
return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run()
|
||||
}
|
||||
|
||||
func (self *CommitCommands) commitMessageArgs(message string) string {
|
||||
msg, description, _ := strings.Cut(message, "\n")
|
||||
descriptionArgs := ""
|
||||
if description != "" {
|
||||
descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs)
|
||||
}
|
||||
|
||||
// runs git commit without the -m argument meaning it will invoke the user's editor
|
||||
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s", self.signoffFlag()))
|
||||
return self.cmd.New(fmt.Sprintf("git commit%s%s", self.signoffFlag(), self.verboseFlag()))
|
||||
}
|
||||
|
||||
func (self *CommitCommands) signoffFlag() string {
|
||||
@@ -73,6 +82,17 @@ func (self *CommitCommands) signoffFlag() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitCommands) verboseFlag() string {
|
||||
switch self.config.UserConfig.Git.Commit.Verbose {
|
||||
case "always":
|
||||
return " --verbose"
|
||||
case "never":
|
||||
return " --no-verbose"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
|
||||
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput()
|
||||
@@ -138,14 +158,19 @@ func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git commit --amend --no-edit --allow-empty")
|
||||
}
|
||||
|
||||
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
|
||||
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
ignoreWhitespaceArg := ""
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceArg = " --ignore-all-space"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", self.UserConfig.Git.Paging.ColorArg, contextSize, sha, filterPathArg)
|
||||
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --stat -p %s%s%s",
|
||||
self.UserConfig.Git.Paging.ColorArg, contextSize, sha, ignoreWhitespaceArg, filterPathArg)
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
}
|
||||
|
||||
@@ -162,3 +187,13 @@ func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
|
||||
func (self *CommitCommands) CreateFixupCommit(sha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
|
||||
}
|
||||
|
||||
// a value of 0 means the head commit, 1 is the parent commit, etc
|
||||
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
|
||||
hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput()
|
||||
formattedHash := strings.TrimSpace(hash)
|
||||
if len(formattedHash) == 0 {
|
||||
return "", ErrInvalidCommitIndex
|
||||
}
|
||||
return self.GetCommitMessage(formattedHash)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitFileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
|
||||
return &CommitFileLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
cmdStr := NewGitCmd("diff").
|
||||
Arg("--submodule").
|
||||
Arg("--no-ext-diff").
|
||||
Arg("--name-status").
|
||||
Arg("-z").
|
||||
Arg("--no-renames").
|
||||
ArgIf(reverse, "-R").
|
||||
Arg(from).
|
||||
Arg(to).
|
||||
ToString()
|
||||
|
||||
filenames, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
|
||||
// so we need to split it by the null character and then map each status-name pair to a commit file
|
||||
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(lines) == 1 {
|
||||
return []*models.CommitFile{}
|
||||
}
|
||||
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
|
||||
return &models.CommitFile{
|
||||
ChangeStatus: chunk[0],
|
||||
Name: chunk[1],
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCommitFilesFromFilenames(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
input string
|
||||
output []*models.CommitFile
|
||||
}{
|
||||
{
|
||||
testName: "no files",
|
||||
input: "",
|
||||
output: []*models.CommitFile{},
|
||||
},
|
||||
{
|
||||
testName: "one file",
|
||||
input: "MM\x00Myfile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "two files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "three files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
{
|
||||
Name: "YetAnother",
|
||||
ChangeStatus: " M",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
result := getCommitFilesFromFilenames(test.input)
|
||||
assert.Equal(t, test.output, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// CommitLoader returns a list of Commit objects for the current repo
|
||||
type CommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
// List of main branches that exist in the repo, quoted for direct use in a git command.
|
||||
// We use these to obtain the merge base of the branch.
|
||||
// When nil, we're yet to obtain the list of main branches.
|
||||
quotedMainBranches *string
|
||||
}
|
||||
|
||||
// making our dependencies explicit for the sake of easier testing
|
||||
func NewCommitLoader(
|
||||
cmn *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dotGitDir string,
|
||||
getRebaseMode func() (enums.RebaseMode, error),
|
||||
) *CommitLoader {
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: os.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
dotGitDir: dotGitDir,
|
||||
quotedMainBranches: nil,
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
// determines if we show the whole git graph i.e. pass the '--all' flag
|
||||
All bool
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
passedFirstPushedCommit := false
|
||||
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
|
||||
if err != nil {
|
||||
// must have no upstream branch so we'll consider everything as pushed
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
|
||||
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]models.CommitStatus{true: models.StatusUnpushed, false: models.StatusPushed}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
commits = self.setCommitMergedStatuses(opts.RefName, commits)
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (self *CommitLoader) 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.IsTODO() { // removing the existing rebase commits so we can add the refreshed ones
|
||||
result = append(result, commits[i:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode == enums.REBASE_MODE_NONE {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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 (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
||||
split := strings.SplitN(line, "\x00", 7)
|
||||
|
||||
sha := split[0]
|
||||
unixTimestamp := split[1]
|
||||
authorName := split[2]
|
||||
authorEmail := split[3]
|
||||
extraInfo := strings.TrimSpace(split[4])
|
||||
parentHashes := split[5]
|
||||
message := split[6]
|
||||
|
||||
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)
|
||||
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
AuthorName: authorName,
|
||||
AuthorEmail: authorEmail,
|
||||
Parents: parents,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
commits, err := self.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := slices.FilterMap(commits, func(commit *models.Commit) (string, bool) {
|
||||
return commit.Sha, commit.Sha != ""
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
NewGitCmd("show").
|
||||
Config("log.showSignature=false").
|
||||
Arg("--no-patch").
|
||||
Arg("--oneline").
|
||||
Arg(strings.Join(commitShas, " ")).
|
||||
Arg(prettyFormat).
|
||||
Arg("--abbrev=20").
|
||||
ToString(),
|
||||
).DontLog()
|
||||
|
||||
fullCommits := map[string]*models.Commit{}
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
fullCommits[commit.Sha] = commit
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
for _, rebasingCommit := range commits {
|
||||
if rebasingCommit.Sha == "" {
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := fullCommits[rebasingCommit.Sha]; commit != nil {
|
||||
commit.Action = rebasingCommit.Action
|
||||
commit.Status = rebasingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
}
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case enums.REBASE_MODE_MERGING:
|
||||
return self.getNormalRebasingCommits()
|
||||
case enums.REBASE_MODE_INTERACTIVE:
|
||||
return self.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := self.readFile(filepath.Join(self.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 = self.walkFiles(filepath.Join(self.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 := self.readFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := string(bytesContent)
|
||||
commit := self.commitFromPatch(content)
|
||||
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 (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
|
||||
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
|
||||
if err != nil {
|
||||
self.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{}
|
||||
|
||||
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Command == todo.UpdateRef {
|
||||
t.Msg = strings.TrimPrefix(t.Ref, "refs/heads/")
|
||||
} else if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = slices.Prepend(commits, &models.Commit{
|
||||
Sha: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
})
|
||||
}
|
||||
|
||||
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 (self *CommitLoader) commitFromPatch(content string) *models.Commit {
|
||||
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: models.StatusRebasing,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) []*models.Commit {
|
||||
ancestor := self.getMergeBase(refName)
|
||||
if ancestor == "" {
|
||||
return commits
|
||||
}
|
||||
passedAncestor := false
|
||||
for i, commit := range commits {
|
||||
if strings.HasPrefix(ancestor, commit.Sha) {
|
||||
passedAncestor = true
|
||||
}
|
||||
if commit.Status != models.StatusPushed && commit.Status != models.StatusUnpushed {
|
||||
continue
|
||||
}
|
||||
if passedAncestor {
|
||||
commits[i].Status = models.StatusMerged
|
||||
}
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getMergeBase(refName string) string {
|
||||
if self.quotedMainBranches == nil {
|
||||
self.quotedMainBranches = lo.ToPtr(self.getExistingMainBranches())
|
||||
}
|
||||
|
||||
if *self.quotedMainBranches == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// We pass all configured main branches to the merge-base call; git will
|
||||
// return the base commit for the closest one.
|
||||
|
||||
output, err := self.cmd.New(
|
||||
NewGitCmd("merge-base").
|
||||
Arg(self.cmd.Quote(refName)).
|
||||
Arg(*self.quotedMainBranches).
|
||||
ToString(),
|
||||
).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
// If there's an error, it must be because one of the main branches that
|
||||
// used to exist when we called getExistingMainBranches() was deleted
|
||||
// meanwhile. To fix this for next time, throw away our cache.
|
||||
self.quotedMainBranches = nil
|
||||
}
|
||||
return ignoringWarnings(output)
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getExistingMainBranches() string {
|
||||
return strings.Join(
|
||||
lo.FilterMap(self.UserConfig.Git.MainBranches,
|
||||
func(branchName string, _ int) (string, bool) {
|
||||
quotedRef := self.cmd.Quote("refs/heads/" + branchName)
|
||||
if err := self.cmd.New(
|
||||
NewGitCmd("rev-parse").Arg("--verify").Arg("--quiet").Arg(quotedRef).ToString(),
|
||||
).DontLog().Run(); err != nil {
|
||||
return "", false
|
||||
}
|
||||
return quotedRef, true
|
||||
}), " ")
|
||||
}
|
||||
|
||||
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 (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := self.cmd.
|
||||
New(
|
||||
NewGitCmd("merge-base").
|
||||
Arg(self.cmd.Quote(refName)).
|
||||
Arg(self.cmd.Quote(strings.TrimPrefix(refName, "refs/heads/")) + "@{u}").
|
||||
ToString(),
|
||||
).
|
||||
DontLog().
|
||||
RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
config := self.UserConfig.Git.Log
|
||||
|
||||
cmdStr := NewGitCmd("log").
|
||||
Arg(self.cmd.Quote(opts.RefName)).
|
||||
ArgIf(config.Order != "default", "--"+config.Order).
|
||||
ArgIf(opts.All, "--all").
|
||||
Arg("--oneline").
|
||||
Arg(prettyFormat).
|
||||
Arg("--abbrev=40").
|
||||
ArgIf(opts.Limit, "-300").
|
||||
ArgIf(opts.FilterPath != "", "--follow").
|
||||
ArgIf(opts.FilterPath != "", self.cmd.Quote(opts.FilterPath)).
|
||||
Arg("--no-show-signature").
|
||||
ToString()
|
||||
|
||||
return self.cmd.New(cmdStr)
|
||||
}
|
||||
|
||||
const prettyFormat = `--pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s"`
|
||||
@@ -0,0 +1,321 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
var singleCommitOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
logOrder string
|
||||
rebaseMode enums.RebaseMode
|
||||
opts GetCommitsOptions
|
||||
mainBranches []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should use proper upstream name for branch",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "refs/heads/mybranch", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "refs/heads/mybranch" "mybranch"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "refs/heads/mybranch" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, commitsOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/master"`, "", nil). // this one does
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/main"`, "", errors.New("error")). // this one doesn't
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "refs/heads/master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
|
||||
Name: "fix logging",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640824515,
|
||||
Parents: []string{
|
||||
"e94e8fc5b6fab4cb755f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
|
||||
Name: "refactor",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640823749,
|
||||
Parents: []string{
|
||||
"d8084cd558925eb7c9c3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
|
||||
Name: "WIP",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821426,
|
||||
Parents: []string{
|
||||
"65f910ebd85283b5cce9",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
|
||||
Name: "WIP",
|
||||
Status: models.StatusPushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821275,
|
||||
Parents: []string{
|
||||
"26c07b1ab33860a1a759",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
|
||||
Name: "WIP",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640750752,
|
||||
Parents: []string{
|
||||
"3d4470a6c072208722e5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
|
||||
Name: "WIP",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640748818,
|
||||
Parents: []string{
|
||||
"053a66a7be3da43aacdc",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
|
||||
Name: "refactoring the config struct",
|
||||
Status: models.StatusMerged,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640739815,
|
||||
Parents: []string{
|
||||
"985fe482e806b172aea4",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should not call merge-base for mainBranches if none exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist; neither does
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/master"`, "", errors.New("error")).
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/main"`, "", errors.New("error")),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should call merge-base for all main branches that exist",
|
||||
logOrder: "topo-order",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
mainBranches: []string{"master", "main", "develop", "1.0-hotfixes"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, singleCommitOutput, nil).
|
||||
// here it's testing which of the configured main branches exist
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/master"`, "", nil).
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/main"`, "", errors.New("error")).
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/develop"`, "", nil).
|
||||
Expect(`git rev-parse --verify --quiet "refs/heads/1.0-hotfixes"`, "", nil).
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "refs/heads/master" "refs/heads/develop" "refs/heads/1.0-hotfixes"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: models.StatusUnpushed,
|
||||
Action: models.ActionNone,
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should not specify order if `log.order` is `default`",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --no-show-signature --`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should set filter path",
|
||||
logOrder: "default",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
opts: GetCommitsOptions{RefName: "HEAD", FilterPath: "src"},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "HEAD" --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40 --follow --no-show-signature -- "src"`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
common := utils.NewDummyCommon()
|
||||
common.UserConfig.Git.Log.Order = scenario.logOrder
|
||||
|
||||
builder := &CommitLoader{
|
||||
Common: common,
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
walkFiles: func(root string, fn filepath.WalkFunc) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
common.UserConfig.Git.MainBranches = scenario.mainBranches
|
||||
commits, err := builder.GetCommits(scenario.opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,32 @@ import (
|
||||
)
|
||||
|
||||
func TestCommitRewordCommit(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
|
||||
instance := buildCommitCommands(commonDeps{runner: runner})
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
input string
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Single line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil),
|
||||
"test",
|
||||
},
|
||||
{
|
||||
"Multi line reword",
|
||||
oscommands.NewFakeRunner(t).ExpectGitArgs([]string{"commit", "--allow-empty", "--amend", "--only", "-m", "test", "-m", "line 2\nline 3"}, "", nil),
|
||||
"test\nline 2\nline 3",
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
assert.NoError(t, instance.RewordLastCommit("test"))
|
||||
runner.CheckForMissingCalls()
|
||||
assert.NoError(t, instance.RewordLastCommit(s.input))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitResetToCommit(t *testing.T) {
|
||||
@@ -27,7 +47,7 @@ func TestCommitResetToCommit(t *testing.T) {
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestCommitCommitObj(t *testing.T) {
|
||||
func TestCommitCommitCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
@@ -89,6 +109,62 @@ func TestCommitCommitObj(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitCommitEditorCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
configSignoff bool
|
||||
configVerbose string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Commit using editor",
|
||||
configSignoff: false,
|
||||
configVerbose: "default",
|
||||
expected: `git commit`,
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "never",
|
||||
expected: `git commit --no-verbose`,
|
||||
},
|
||||
{
|
||||
testName: "Commit with --verbose flag",
|
||||
configSignoff: false,
|
||||
configVerbose: "always",
|
||||
expected: `git commit --verbose`,
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff",
|
||||
configSignoff: true,
|
||||
configVerbose: "default",
|
||||
expected: `git commit --signoff`,
|
||||
},
|
||||
{
|
||||
testName: "Commit with --signoff and --no-verbose",
|
||||
configSignoff: true,
|
||||
configVerbose: "never",
|
||||
expected: `git commit --signoff --no-verbose`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.Git.Commit.SignOff = s.configSignoff
|
||||
userConfig.Git.Commit.Verbose = s.configVerbose
|
||||
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
|
||||
|
||||
cmdStr := instance.CommitEditorCmdObj().ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommitCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
@@ -121,30 +197,41 @@ func TestCommitCreateFixupCommit(t *testing.T) {
|
||||
|
||||
func TestCommitShowCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
contextSize int
|
||||
expected string
|
||||
testName string
|
||||
filterPath string
|
||||
contextSize int
|
||||
ignoreWhitespace bool
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case without filter path",
|
||||
filterPath: "",
|
||||
contextSize: 3,
|
||||
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
|
||||
testName: "Default case without filter path",
|
||||
filterPath: "",
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: "git show --submodule --color=always --unified=3 --stat -p 1234567890",
|
||||
},
|
||||
{
|
||||
testName: "Default case with filter path",
|
||||
filterPath: "file.txt",
|
||||
contextSize: 3,
|
||||
expected: `git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- "file.txt"`,
|
||||
testName: "Default case with filter path",
|
||||
filterPath: "file.txt",
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: `git show --submodule --color=always --unified=3 --stat -p 1234567890 -- "file.txt"`,
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
|
||||
testName: "Show diff with custom context size",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: false,
|
||||
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890",
|
||||
},
|
||||
{
|
||||
testName: "Show diff, ignoring whitespace",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: true,
|
||||
expected: "git show --submodule --color=always --unified=77 --stat -p 1234567890 --ignore-all-space",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -156,7 +243,7 @@ func TestCommitShowCmdObj(t *testing.T) {
|
||||
|
||||
instance := buildCommitCommands(commonDeps{userConfig: userConfig})
|
||||
|
||||
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath).ToString()
|
||||
cmdStr := instance.ShowCmdObj("1234567890", s.filterPath, s.ignoreWhitespace).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
@@ -207,3 +294,40 @@ Merge pull request #1750 from mark2185/fix-issue-template
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitMessageFromHistory(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(string, error)
|
||||
}
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Empty message",
|
||||
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "", nil).Expect("git rev-list --format=%B --max-count=1 ", "", nil),
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Default case to retrieve a commit in history",
|
||||
oscommands.NewFakeRunner(t).Expect("git log -1 --skip=2 --pretty=%H", "sha3 \n", nil).Expect("git rev-list --format=%B --max-count=1 sha3", `commit sha3
|
||||
use generics to DRY up context code`, nil),
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "use generics to DRY up context code", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildCommitCommands(commonDeps{runner: s.runner})
|
||||
|
||||
output, err := instance.GetCommitMessageFromHistory(2)
|
||||
|
||||
s.test(output, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type GitCommon struct {
|
||||
*common.Common
|
||||
version *GitVersion
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
os *oscommands.OSCommand
|
||||
dotGitDir string
|
||||
repo *gogit.Repository
|
||||
config *ConfigCommands
|
||||
// mutex for doing things like push/pull/fetch
|
||||
syncMutex *sync.Mutex
|
||||
syncMutex *deadlock.Mutex
|
||||
}
|
||||
|
||||
func NewGitCommon(
|
||||
cmn *common.Common,
|
||||
version *GitVersion,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
osCommand *oscommands.OSCommand,
|
||||
dotGitDir string,
|
||||
repo *gogit.Repository,
|
||||
config *ConfigCommands,
|
||||
syncMutex *sync.Mutex,
|
||||
syncMutex *deadlock.Mutex,
|
||||
) *GitCommon {
|
||||
return &GitCommon{
|
||||
Common: cmn,
|
||||
version: version,
|
||||
cmd: cmd,
|
||||
os: osCommand,
|
||||
dotGitDir: dotGitDir,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
type commonDeps struct {
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
userConfig *config.UserConfig
|
||||
gitVersion *GitVersion
|
||||
gitConfig *git_config.FakeGitConfig
|
||||
getenv func(string) string
|
||||
removeFile func(string) error
|
||||
@@ -47,6 +49,11 @@ func buildGitCommon(deps commonDeps) *GitCommon {
|
||||
gitCommon.Common.UserConfig = config.GetDefaultConfig()
|
||||
}
|
||||
|
||||
gitCommon.version = deps.gitVersion
|
||||
if gitCommon.version == nil {
|
||||
gitCommon.version = &GitVersion{2, 0, 0, ""}
|
||||
}
|
||||
|
||||
gitConfig := deps.gitConfig
|
||||
if gitConfig == nil {
|
||||
gitConfig = git_config.NewFakeGitConfig(nil)
|
||||
@@ -70,6 +77,7 @@ func buildGitCommon(deps commonDeps) *GitCommon {
|
||||
GetenvFn: getenv,
|
||||
Cmd: cmd,
|
||||
RemoveFileFn: removeFile,
|
||||
TempDir: os.TempDir(),
|
||||
})
|
||||
|
||||
gitCommon.dotGitDir = deps.dotGitDir
|
||||
@@ -86,8 +94,8 @@ func buildRepo() *gogit.Repository {
|
||||
return repo
|
||||
}
|
||||
|
||||
func buildFileLoader(gitCommon *GitCommon) *loaders.FileLoader {
|
||||
return loaders.NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
|
||||
func buildFileLoader(gitCommon *GitCommon) *FileLoader {
|
||||
return NewFileLoader(gitCommon.Common, gitCommon.cmd, gitCommon.config)
|
||||
}
|
||||
|
||||
func buildSubmoduleCommands(deps commonDeps) *SubmoduleCommands {
|
||||
@@ -142,3 +150,9 @@ func buildBranchCommands(deps commonDeps) *BranchCommands {
|
||||
|
||||
return NewBranchCommands(gitCommon)
|
||||
}
|
||||
|
||||
func buildFlowCommands(deps commonDeps) *FlowCommands {
|
||||
gitCommon := buildGitCommon(deps)
|
||||
|
||||
return NewFlowCommands(gitCommon)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -20,14 +22,14 @@ func NewFileCommands(gitCommon *GitCommon) *FileCommands {
|
||||
|
||||
// Cat obtains the content of a file
|
||||
func (self *FileCommands) Cat(fileName string) (string, error) {
|
||||
buf, err := ioutil.ReadFile(fileName)
|
||||
buf, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string, error) {
|
||||
func (self *FileCommands) GetEditCmdStrLegacy(filename string, lineNumber int) (string, error) {
|
||||
editor := self.UserConfig.OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
@@ -72,3 +74,82 @@ func (self *FileCommands) GetEditCmdStr(filename string, lineNumber int) (string
|
||||
}
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditCmdStr(filename string) (string, bool) {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.Edit == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, 1); err == nil {
|
||||
return cmdStr, true
|
||||
}
|
||||
}
|
||||
|
||||
template, editInTerminal := config.GetEditTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr, editInTerminal
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditAtLineCmdStr(filename string, lineNumber int) (string, bool) {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.EditAtLine == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
|
||||
return cmdStr, true
|
||||
}
|
||||
}
|
||||
|
||||
template, editInTerminal := config.GetEditAtLineTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr, editInTerminal
|
||||
}
|
||||
|
||||
func (self *FileCommands) GetEditAtLineAndWaitCmdStr(filename string, lineNumber int) string {
|
||||
// Legacy support for old config; to be removed at some point
|
||||
if self.UserConfig.OS.EditAtLineAndWait == "" && self.UserConfig.OS.EditCommandTemplate != "" {
|
||||
if cmdStr, err := self.GetEditCmdStrLegacy(filename, lineNumber); err == nil {
|
||||
return cmdStr
|
||||
}
|
||||
}
|
||||
|
||||
template := config.GetEditAtLineAndWaitTemplate(&self.UserConfig.OS, self.guessDefaultEditor)
|
||||
|
||||
templateValues := map[string]string{
|
||||
"filename": self.cmd.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
cmdStr := utils.ResolvePlaceholderString(template, templateValues)
|
||||
return cmdStr
|
||||
}
|
||||
|
||||
func (self *FileCommands) guessDefaultEditor() string {
|
||||
// Try to query a few places where editors get configured
|
||||
editor := self.config.GetCoreEditor()
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = self.os.Getenv("EDITOR")
|
||||
}
|
||||
|
||||
if editor != "" {
|
||||
// At this point, it might be more than just the name of the editor;
|
||||
// e.g. it might be "code -w" or "vim -u myvim.rc". So assume that
|
||||
// everything up to the first space is the editor name.
|
||||
editor = strings.Split(editor, " ")[0]
|
||||
}
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type FileLoaderConfig interface {
|
||||
GetShowUntrackedFiles() string
|
||||
}
|
||||
|
||||
type FileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
config FileLoaderConfig
|
||||
getFileType func(string) string
|
||||
}
|
||||
|
||||
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
|
||||
return &FileLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getFileType: oscommands.FileType,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
self.Log.Error(err)
|
||||
}
|
||||
files := []*models.File{}
|
||||
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
|
||||
file := &models.File{
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
Type: self.getFileType(status.Name),
|
||||
}
|
||||
|
||||
models.SetStatusFields(file, status.Change)
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the file status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
type FileStatus struct {
|
||||
StatusString string
|
||||
Change string // ??, MM, AM, ...
|
||||
Name string
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = " --no-renames"
|
||||
}
|
||||
|
||||
statusLines, _, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutputs()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
response := []FileStatus{}
|
||||
|
||||
for i := 0; i < len(splitLines); i++ {
|
||||
original := splitLines[i]
|
||||
|
||||
if len(original) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
status := FileStatus{
|
||||
StatusString: original,
|
||||
Change: original[:2],
|
||||
Name: original[3:],
|
||||
PreviousName: "",
|
||||
}
|
||||
|
||||
if strings.HasPrefix(status.Change, "R") {
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
status.PreviousName = splitLines[i+1]
|
||||
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
|
||||
i++
|
||||
}
|
||||
|
||||
response = append(response, status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedFiles []*models.File
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
|
||||
[]*models.File{},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM a\nb.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "after1.txt",
|
||||
PreviousName: "before1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "R before1.txt -> after1.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "R ",
|
||||
},
|
||||
{
|
||||
Name: "after2.txt",
|
||||
PreviousName: "before2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "RM before2.txt -> after2.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
`?? a -> b.txt`,
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "a -> b.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? a -> b.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := &FileLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: cmd,
|
||||
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
|
||||
getFileType: func(string) string { return "file" },
|
||||
}
|
||||
|
||||
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type FakeFileLoaderConfig struct {
|
||||
showUntrackedFiles string
|
||||
}
|
||||
|
||||
func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
|
||||
return self.showUntrackedFiles
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
func TestEditFileCmdStrLegacy(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
configEditCommand string
|
||||
@@ -172,7 +172,206 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
getenv: s.getenv,
|
||||
})
|
||||
|
||||
s.test(instance.GetEditCmdStr(s.filename, 1))
|
||||
s.test(instance.GetEditCmdStrLegacy(s.filename, 1))
|
||||
s.runner.CheckForMissingCalls()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
expectedEditInTerminal bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim -- "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
osConfig: config.OSConfig{
|
||||
Edit: "nano {{filename}}",
|
||||
},
|
||||
expectedCmdStr: `nano "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl -- "file/with space"`,
|
||||
expectedEditInTerminal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr, editInTerminal := instance.GetEditCmdStr(s.filename)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileAtLineCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
lineNumber int
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
expectedEditInTerminal bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 42,
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim +42 -- "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 35,
|
||||
osConfig: config.OSConfig{
|
||||
EditAtLine: "nano +{{line}} {{filename}}",
|
||||
},
|
||||
expectedCmdStr: `nano +35 "test"`,
|
||||
expectedEditInTerminal: true,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
lineNumber: 12,
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl -- "file/with space":12`,
|
||||
expectedEditInTerminal: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr, editInTerminal := instance.GetEditAtLineCmdStr(s.filename, s.lineNumber)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
assert.Equal(t, s.expectedEditInTerminal, editInTerminal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFileAtLineAndWaitCmd(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
lineNumber int
|
||||
osConfig config.OSConfig
|
||||
expectedCmdStr string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
lineNumber: 42,
|
||||
osConfig: config.OSConfig{},
|
||||
expectedCmdStr: `vim +42 -- "test"`,
|
||||
},
|
||||
{
|
||||
filename: "file/with space",
|
||||
lineNumber: 12,
|
||||
osConfig: config.OSConfig{
|
||||
EditPreset: "sublime",
|
||||
},
|
||||
expectedCmdStr: `subl --wait -- "file/with space":12`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
userConfig := config.GetDefaultConfig()
|
||||
userConfig.OS = s.osConfig
|
||||
|
||||
instance := buildFileCommands(commonDeps{
|
||||
userConfig: userConfig,
|
||||
})
|
||||
|
||||
cmdStr := instance.GetEditAtLineAndWaitCmdStr(s.filename, s.lineNumber)
|
||||
assert.Equal(t, s.expectedCmdStr, cmdStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGuessDefaultEditor(t *testing.T) {
|
||||
type scenario struct {
|
||||
gitConfigMockResponses map[string]string
|
||||
getenv func(string) string
|
||||
expectedResult string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: map[string]string{"core.editor": "nano"},
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "nano",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: map[string]string{"core.editor": "code -w"},
|
||||
getenv: func(env string) string {
|
||||
return ""
|
||||
},
|
||||
expectedResult: "code",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
expectedResult: "emacs",
|
||||
},
|
||||
{
|
||||
gitConfigMockResponses: nil,
|
||||
getenv: func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "bbedit -w"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
expectedResult: "bbedit",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
instance := buildFileCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
getenv: s.getenv,
|
||||
})
|
||||
|
||||
assert.Equal(t, s.expectedResult, instance.guessDefaultEditor())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
branchType string
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "basic",
|
||||
branchType: "feature",
|
||||
name: "test",
|
||||
expected: "git flow feature start test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{})
|
||||
|
||||
assert.Equal(t,
|
||||
instance.StartCmdObj(s.branchType, s.name).ToString(),
|
||||
s.expected,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinishCmdObj(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
branchName string
|
||||
expected string
|
||||
expectedError string
|
||||
gitConfigMockResponses map[string]string
|
||||
}{
|
||||
{
|
||||
testName: "not a git flow branch",
|
||||
branchName: "mybranch",
|
||||
expected: "",
|
||||
expectedError: "This does not seem to be a git flow branch",
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "feature branch without config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: "",
|
||||
expectedError: "This does not seem to be a git flow branch",
|
||||
gitConfigMockResponses: nil,
|
||||
},
|
||||
{
|
||||
testName: "feature branch with config",
|
||||
branchName: "feature/mybranch",
|
||||
expected: "git flow feature finish mybranch",
|
||||
expectedError: "",
|
||||
gitConfigMockResponses: map[string]string{
|
||||
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildFlowCommands(commonDeps{
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
|
||||
cmd, err := instance.FinishCmdObj(s.branchName)
|
||||
|
||||
if s.expectedError != "" {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
} else {
|
||||
assert.Equal(t, err.Error(), s.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cmd.ToString(), s.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package git_commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
// convenience struct for building git commands. Especially useful when
|
||||
// including conditional args
|
||||
type GitCommandBuilder struct {
|
||||
command string
|
||||
}
|
||||
|
||||
func NewGitCmd(command string) *GitCommandBuilder {
|
||||
return &GitCommandBuilder{command: command}
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Arg(flag string) *GitCommandBuilder {
|
||||
if flag == "" {
|
||||
return self
|
||||
}
|
||||
|
||||
self.command += " " + flag
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ArgIf(include bool, flag string) *GitCommandBuilder {
|
||||
if include {
|
||||
return self.Arg(flag)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ArgIfElse(isTrue bool, onTrue string, onFalse string) *GitCommandBuilder {
|
||||
if isTrue {
|
||||
return self.Arg(onTrue)
|
||||
} else {
|
||||
return self.Arg(onFalse)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Args(args []string) *GitCommandBuilder {
|
||||
for _, arg := range args {
|
||||
self.Arg(arg)
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
|
||||
// config settings come before the command
|
||||
self.command = fmt.Sprintf("-c %s %s", value, self.command)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
||||
// repo path comes before the command
|
||||
self.command = fmt.Sprintf("-C %s %s", value, self.command)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) ToString() string {
|
||||
return "git " + self.command
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitCommandBuilder(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: NewGitCmd("push").
|
||||
Arg("--force-with-lease").
|
||||
Arg("--set-upstream").
|
||||
Arg("origin").
|
||||
Arg("master").
|
||||
ToString(),
|
||||
expected: "git push --force-with-lease --set-upstream origin master",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIf(true, "--test").ToString(),
|
||||
expected: "git push --test",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIf(false, "--test").ToString(),
|
||||
expected: "git push",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIfElse(true, "-b", "-a").ToString(),
|
||||
expected: "git push -b",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").ArgIfElse(false, "-a", "-b").ToString(),
|
||||
expected: "git push -b",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").Args([]string{"-a", "-b"}).ToString(),
|
||||
expected: "git push -a -b",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").Config("user.name=foo").Config("user.email=bar").ToString(),
|
||||
expected: "git -c user.email=bar -c user.name=foo push",
|
||||
},
|
||||
{
|
||||
input: NewGitCmd("push").RepoPath("a/b/c").ToString(),
|
||||
expected: "git -C a/b/c push",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.Equal(t, s.input, s.expected)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package git_commands
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
@@ -16,7 +18,7 @@ type PatchCommands struct {
|
||||
status *StatusCommands
|
||||
stash *StashCommands
|
||||
|
||||
PatchManager *patch.PatchManager
|
||||
PatchBuilder *patch.PatchBuilder
|
||||
}
|
||||
|
||||
func NewPatchCommands(
|
||||
@@ -25,7 +27,7 @@ func NewPatchCommands(
|
||||
commit *CommitCommands,
|
||||
status *StatusCommands,
|
||||
stash *StashCommands,
|
||||
patchManager *patch.PatchManager,
|
||||
patchBuilder *patch.PatchBuilder,
|
||||
) *PatchCommands {
|
||||
return &PatchCommands{
|
||||
GitCommon: gitCommon,
|
||||
@@ -33,7 +35,7 @@ func NewPatchCommands(
|
||||
commit: commit,
|
||||
status: status,
|
||||
stash: stash,
|
||||
PatchManager: patchManager,
|
||||
PatchBuilder: patchBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,10 +46,8 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -72,10 +72,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
}
|
||||
|
||||
// apply each patch forward
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.PatchBuilder.ApplyPatches(false); err != nil {
|
||||
// Don't abort the rebase here; this might cause conflicts, so give
|
||||
// the user a chance to resolve them
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,7 +84,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -106,24 +105,24 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
|
||||
todoLines := self.rebase.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
return "edit"
|
||||
} else {
|
||||
return "pick"
|
||||
}
|
||||
})
|
||||
changes := []daemon.ChangeTodoAction{
|
||||
{Sha: commits[sourceCommitIdx].Sha, NewAction: todo.Edit},
|
||||
{Sha: commits[destinationCommitIdx].Sha, NewAction: todo.Edit},
|
||||
}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todoLines, true).Run()
|
||||
err := self.rebase.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: commits[baseIndex].Sha,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -132,6 +131,12 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
return err
|
||||
}
|
||||
|
||||
patch, err := self.diffHeadAgainstCommit(commits[sourceCommitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if self.rebase.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
@@ -139,10 +144,9 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
self.rebase.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 := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
|
||||
// Don't abort the rebase here; this might cause conflicts, so give
|
||||
// the user a chance to resolve them
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -152,7 +156,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,11 +177,9 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -187,17 +189,21 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
return err
|
||||
}
|
||||
|
||||
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if self.rebase.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
self.rebase.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
|
||||
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = self.rebase.AbortRebase()
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -208,7 +214,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
|
||||
}
|
||||
}
|
||||
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -220,10 +226,8 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.PatchManager.ApplyPatches(true); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := self.PatchBuilder.ApplyPatches(true); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -232,18 +236,20 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
return err
|
||||
}
|
||||
|
||||
// add patches to index
|
||||
if err := self.PatchManager.ApplyPatches(false); err != nil {
|
||||
if err := self.rebase.AbortRebase(); err != nil {
|
||||
return err
|
||||
}
|
||||
patch, err := self.diffHeadAgainstCommit(commits[commitIdx])
|
||||
if err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil {
|
||||
_ = self.rebase.AbortRebase()
|
||||
return err
|
||||
}
|
||||
|
||||
head_message, _ := self.commit.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
err := self.commit.CommitCmdObj(new_message).Run()
|
||||
if err != nil {
|
||||
if err := self.commit.CommitCmdObj(new_message).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -251,6 +257,15 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
self.PatchManager.Reset()
|
||||
self.PatchBuilder.Reset()
|
||||
return self.rebase.ContinueRebase()
|
||||
}
|
||||
|
||||
// We have just applied a patch in reverse to discard it from a commit; if we
|
||||
// now try to apply the patch again to move it to a later commit, or to the
|
||||
// index, then this would conflict "with itself" in case the patch contained
|
||||
// only some lines of a range of adjacent added lines. To solve this, we
|
||||
// get the diff of HEAD and the original commit and then apply that.
|
||||
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
|
||||
return self.cmd.New(fmt.Sprintf("git diff HEAD..%s", commit.Sha)).RunWithOutput()
|
||||
}
|
||||
|
||||
+156
-145
@@ -2,15 +2,17 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type RebaseCommands struct {
|
||||
@@ -34,7 +36,7 @@ func NewRebaseCommands(
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, message string) error {
|
||||
if index == 0 {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return self.commit.RewordLastCommit(message)
|
||||
}
|
||||
@@ -54,12 +56,16 @@ func (self *RebaseCommands) RewordCommit(commits []*models.Commit, index int, me
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) RewordCommitInEditor(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[index].Sha,
|
||||
NewAction: todo.Reword,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, false), nil
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, index+1),
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) ResetCommitAuthor(commits []*models.Commit, index int) error {
|
||||
@@ -75,7 +81,7 @@ func (self *RebaseCommands) SetCommitAuthor(commits []*models.Commit, index int,
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f func() error) error {
|
||||
if index == 0 {
|
||||
if models.IsHeadCommit(commits, index) {
|
||||
// we've selected the top commit so no rebase is required
|
||||
return f()
|
||||
}
|
||||
@@ -95,33 +101,79 @@ func (self *RebaseCommands) GenericAmend(commits []*models.Commit, index int, f
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) 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(self.Tr.NoRoom)
|
||||
}
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, index+2)
|
||||
|
||||
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
|
||||
sha := commits[index].Sha
|
||||
|
||||
todoLines := self.BuildTodoLinesSingleAction(orderedCommits, "pick")
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO down: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todoLines, true).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
instruction: daemon.NewMoveTodoDownInstruction(sha),
|
||||
overrideEditor: true,
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action string) error {
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
func (self *RebaseCommands) MoveCommitUp(commits []*models.Commit, index int) error {
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, index+1)
|
||||
|
||||
sha := commits[index].Sha
|
||||
|
||||
self.os.LogCommand(fmt.Sprintf("Moving TODO up: %s", utils.ShortSha(sha)), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
instruction: daemon.NewMoveTodoUpInstruction(sha),
|
||||
overrideEditor: true,
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, index int, action todo.TodoCommand) error {
|
||||
baseIndex := index + 1
|
||||
if action == todo.Squash || action == todo.Fixup {
|
||||
baseIndex++
|
||||
}
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
baseShaOrRoot := getBaseShaOrRoot(commits, baseIndex)
|
||||
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[index].Sha,
|
||||
NewAction: action,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: baseShaOrRoot,
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) EditRebase(branchRef string) error {
|
||||
self.os.LogCommand(fmt.Sprintf("Beginning interactive rebase at '%s'", branchRef), false)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: branchRef,
|
||||
instruction: daemon.NewInsertBreakInstruction(),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func logTodoChanges(changes []daemon.ChangeTodoAction) string {
|
||||
changeTodoStr := strings.Join(slices.Map(changes, func(c daemon.ChangeTodoAction) string {
|
||||
return fmt.Sprintf("%s:%s", c.Sha, c.NewAction)
|
||||
}), "\n")
|
||||
return fmt.Sprintf("Changing TODO actions: %s", changeTodoStr)
|
||||
}
|
||||
|
||||
type PrepareInteractiveRebaseCommandOpts struct {
|
||||
baseShaOrRoot string
|
||||
instruction daemon.Instruction
|
||||
overrideEditor bool
|
||||
}
|
||||
|
||||
// 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 (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todoLines []TodoLine, overrideEditor bool) oscommands.ICmdObj {
|
||||
todo := self.buildTodo(todoLines)
|
||||
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
|
||||
ex := oscommands.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
@@ -129,134 +181,95 @@ func (self *RebaseCommands) PrepareInteractiveRebaseCommand(baseSha string, todo
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
|
||||
self.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
emptyArg := " --empty=keep"
|
||||
if self.version.IsOlderThan(2, 26, 0) {
|
||||
emptyArg = ""
|
||||
}
|
||||
|
||||
rebaseMergesArg := " --rebase-merges"
|
||||
if self.version.IsOlderThan(2, 22, 0) {
|
||||
rebaseMergesArg = ""
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty%s --no-autosquash%s %s",
|
||||
emptyArg, rebaseMergesArg, opts.baseShaOrRoot)
|
||||
self.Log.WithField("command", cmdStr).Debug("RunCommand")
|
||||
|
||||
cmdObj := self.cmd.New(cmdStr)
|
||||
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
|
||||
if opts.instruction != nil {
|
||||
cmdObj.AddEnvVars(daemon.ToEnvVars(opts.instruction)...)
|
||||
} else {
|
||||
self.os.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
gitSequenceEditor = "true"
|
||||
}
|
||||
|
||||
cmdObj.AddEnvVars(
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.InteractiveRebase),
|
||||
daemon.RebaseTODOEnvKey+"="+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 {
|
||||
if opts.overrideEditor {
|
||||
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
|
||||
}
|
||||
|
||||
return cmdObj
|
||||
}
|
||||
|
||||
// produces TodoLines where every commit is picked (or dropped for merge commits) except for the commit at the given index, which
|
||||
// will have the given action applied to it.
|
||||
func (self *RebaseCommands) BuildSingleActionTodo(commits []*models.Commit, actionIndex int, action string) ([]TodoLine, string, error) {
|
||||
baseIndex := actionIndex + 1
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return nil, "", errors.New(self.Tr.CannotRebaseOntoFirstCommit)
|
||||
}
|
||||
|
||||
if action == "squash" || action == "fixup" {
|
||||
baseIndex++
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return nil, "", errors.New(self.Tr.CannotSquashOntoSecondCommit)
|
||||
}
|
||||
}
|
||||
|
||||
todoLines := self.BuildTodoLines(commits[0:baseIndex], func(commit *models.Commit, i int) string {
|
||||
if i == actionIndex {
|
||||
return 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.
|
||||
return "drop"
|
||||
} else {
|
||||
return "pick"
|
||||
}
|
||||
})
|
||||
|
||||
return todoLines, commits[baseIndex].Sha, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
func (self *RebaseCommands) AmendTo(sha string) error {
|
||||
if err := self.commit.CreateFixupCommit(sha); err != nil {
|
||||
func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) error {
|
||||
commit := commits[commitIndex]
|
||||
|
||||
if err := self.commit.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.SquashAllAboveFixupCommits(sha)
|
||||
}
|
||||
|
||||
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(index int, action string) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
// Get the sha of the commit we just created
|
||||
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.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), 0o644)
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewMoveFixupCommitDownInstruction(commit.Sha, fixupSha),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) 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
|
||||
// EditRebaseTodo sets the action for a given rebase commit in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(commit *models.Commit, action todo.TodoCommand) error {
|
||||
return utils.EditRebaseTodo(
|
||||
filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"), commit.Sha, commit.Action, action)
|
||||
}
|
||||
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (self *RebaseCommands) MoveTodoDown(index int) error {
|
||||
func (self *RebaseCommands) MoveTodoDown(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return utils.MoveTodoDown(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
content := strings.Split(string(bytes), "\n")
|
||||
commitCount := self.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), 0o644)
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (self *RebaseCommands) MoveTodoUp(commit *models.Commit) error {
|
||||
fileName := filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo")
|
||||
return utils.MoveTodoUp(fileName, commit.Sha, commit.Action)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
func (self *RebaseCommands) SquashAllAboveFixupCommits(sha string) error {
|
||||
func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) error {
|
||||
shaOrRoot := commit.Sha + "^"
|
||||
if commit.IsFirstCommit() {
|
||||
shaOrRoot = "--root"
|
||||
}
|
||||
|
||||
return self.runSkipEditorCommand(
|
||||
self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --autostash --autosquash %s^",
|
||||
sha,
|
||||
"git rebase --interactive --rebase-merges --autostash --autosquash %s",
|
||||
shaOrRoot,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -276,17 +289,22 @@ func (self *RebaseCommands) BeginInteractiveRebaseForCommit(commits []*models.Co
|
||||
return errors.New(self.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
todo, sha, err := self.BuildSingleActionTodo(commits, commitIndex, "edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changes := []daemon.ChangeTodoAction{{
|
||||
Sha: commits[commitIndex].Sha,
|
||||
NewAction: todo.Edit,
|
||||
}}
|
||||
self.os.LogCommand(logTodoChanges(changes), false)
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand(sha, todo, true).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: getBaseShaOrRoot(commits, commitIndex+1),
|
||||
overrideEditor: true,
|
||||
instruction: daemon.NewChangeTodoActionsInstruction(changes),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (self *RebaseCommands) RebaseBranch(branchName string) error {
|
||||
return self.PrepareInteractiveRebaseCommand(branchName, nil, false).Run()
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{baseShaOrRoot: branchName}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
|
||||
@@ -327,14 +345,16 @@ func (self *RebaseCommands) GenericMergeOrRebaseAction(commandType string, comma
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) runSkipEditorCommand(cmdObj oscommands.ICmdObj) error {
|
||||
instruction := daemon.NewExitImmediatelyInstruction()
|
||||
lazyGitPath := oscommands.GetLazygitPath()
|
||||
return cmdObj.
|
||||
AddEnvVars(
|
||||
daemon.DaemonKindEnvKey+"="+string(daemon.ExitImmediately),
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"GIT_SEQUENCE_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
).
|
||||
AddEnvVars(daemon.ToEnvVars(instruction)...).
|
||||
Run()
|
||||
}
|
||||
|
||||
@@ -368,36 +388,27 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (self *RebaseCommands) CherryPickCommits(commits []*models.Commit) error {
|
||||
todoLines := self.BuildTodoLinesSingleAction(commits, "pick")
|
||||
|
||||
return self.PrepareInteractiveRebaseCommand("HEAD", todoLines, false).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) buildTodo(todoLines []TodoLine) string {
|
||||
lines := slices.Map(todoLines, func(todoLine TodoLine) string {
|
||||
return todoLine.ToString()
|
||||
commitLines := lo.Map(commits, func(commit *models.Commit, _ int) string {
|
||||
return fmt.Sprintf("%s %s", utils.ShortSha(commit.Sha), commit.Name)
|
||||
})
|
||||
self.os.LogCommand(fmt.Sprintf("Cherry-picking commits:\n%s", strings.Join(commitLines, "\n")), false)
|
||||
|
||||
return strings.Join(slices.Reverse(lines), "")
|
||||
return self.PrepareInteractiveRebaseCommand(PrepareInteractiveRebaseCommandOpts{
|
||||
baseShaOrRoot: "HEAD",
|
||||
instruction: daemon.NewCherryPickCommitsInstruction(commits),
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLines(commits []*models.Commit, f func(*models.Commit, int) string) []TodoLine {
|
||||
return slices.MapWithIndex(commits, func(commit *models.Commit, i int) TodoLine {
|
||||
return TodoLine{Action: f(commit, i), Commit: commit}
|
||||
})
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) BuildTodoLinesSingleAction(commits []*models.Commit, action string) []TodoLine {
|
||||
return self.BuildTodoLines(commits, func(commit *models.Commit, i int) string {
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
type TodoLine struct {
|
||||
Action string
|
||||
Commit *models.Commit
|
||||
}
|
||||
|
||||
func (self *TodoLine) ToString() string {
|
||||
return self.Action + " " + self.Commit.Sha + " " + self.Commit.Name + "\n"
|
||||
// we can't start an interactive rebase from the first commit without passing the
|
||||
// '--root' arg
|
||||
func getBaseShaOrRoot(commits []*models.Commit, index int) string {
|
||||
// We assume that the commits slice contains the initial commit of the repo.
|
||||
// Technically this assumption could prove false, but it's unlikely you'll
|
||||
// be starting a rebase from 300 commits ago (which is the original commit limit
|
||||
// at time of writing)
|
||||
if index < len(commits) {
|
||||
return commits[index].Sha
|
||||
} else {
|
||||
return "--root"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package git_commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -15,37 +16,60 @@ import (
|
||||
|
||||
func TestRebaseRebaseBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
testName string
|
||||
arg string
|
||||
gitVersion *GitVersion
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "successful rebase",
|
||||
arg: "master",
|
||||
testName: "successful rebase",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
|
||||
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "unsuccessful rebase",
|
||||
arg: "master",
|
||||
testName: "unsuccessful rebase",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
|
||||
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges master`, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "successful rebase (< 2.26.0)",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 25, 5, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash --rebase-merges master`, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "successful rebase (< 2.22.0)",
|
||||
arg: "master",
|
||||
gitVersion: &GitVersion{2, 21, 9, ""},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty --no-autosquash master`, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{runner: s.runner})
|
||||
instance := buildRebaseCommands(commonDeps{runner: s.runner, gitVersion: s.gitVersion})
|
||||
s.test(instance.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
@@ -62,7 +86,8 @@ func TestRebaseSkipEditorCommand(t *testing.T) {
|
||||
`^VISUAL=.*$`,
|
||||
`^EDITOR=.*$`,
|
||||
`^GIT_EDITOR=.*$`,
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + string(daemon.ExitImmediately) + "$",
|
||||
`^GIT_SEQUENCE_EDITOR=.*$`,
|
||||
"^" + daemon.DaemonKindEnvKey + "=" + strconv.Itoa(int(daemon.DaemonKindExitImmediately)) + "$",
|
||||
} {
|
||||
regexStr := regexStr
|
||||
foundMatch := lo.ContainsBy(envVars, func(envVar string) bool {
|
||||
@@ -124,7 +149,7 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
commitIndex: 0,
|
||||
fileName: "test999.txt",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty abcdef`, "", nil).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty --empty=keep --no-autosquash --rebase-merges abcdef`, "", nil).
|
||||
Expect(`git cat-file -e HEAD^:"test999.txt"`, "", nil).
|
||||
Expect(`git checkout HEAD^ -- "test999.txt"`, "", nil).
|
||||
Expect(`git commit --amend --no-edit --allow-empty`, "", nil).
|
||||
@@ -141,8 +166,9 @@ func TestRebaseDiscardOldFileChanges(t *testing.T) {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
instance := buildRebaseCommands(commonDeps{
|
||||
runner: s.runner,
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
runner: s.runner,
|
||||
gitVersion: &GitVersion{2, 26, 0, ""},
|
||||
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
|
||||
})
|
||||
|
||||
s.test(instance.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type ReflogCommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
|
||||
return &ReflogCommitLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
fields := strings.SplitN(line, "\x00", 4)
|
||||
if len(fields) <= 3 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||
|
||||
parentHashes := fields[3]
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: models.StatusReflog,
|
||||
Parents: parents,
|
||||
}
|
||||
|
||||
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
|
||||
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
|
||||
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
|
||||
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sanity-io/litter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
|
||||
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
|
||||
`, "|", "\x00", -1)
|
||||
|
||||
func TestGetReflogCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
lastReflogCommit *models.Commit
|
||||
filterPath string
|
||||
expectedCommits []*models.Commit
|
||||
expectedOnlyObtainedNew bool
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "no reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "some reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from master to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "f4ddf2f0d4be4ccc7efa",
|
||||
Name: "checkout: moving from A to master",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643149435,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "some reflog entries where last commit is given",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "when passing filterPath",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
filterPath: "path",
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: models.StatusReflog,
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "when command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
filterPath: "",
|
||||
expectedCommits: nil,
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: errors.New("haha"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &ReflogCommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
|
||||
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
t.Logf("actual commits: \n%s", litter.Sdump(commits))
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type RemoteLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error)
|
||||
}
|
||||
|
||||
func NewRemoteLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error),
|
||||
) *RemoteLoader {
|
||||
return &RemoteLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
getGoGitRemotes: getGoGitRemotes,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goGitRemotes, err := self.getGoGitRemotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
|
||||
remoteName := goGitRemote.Config().Name
|
||||
|
||||
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, regexp.QuoteMeta(remoteName)))
|
||||
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
|
||||
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
|
||||
return &models.RemoteBranch{
|
||||
Name: match[1],
|
||||
RemoteName: remoteName,
|
||||
}
|
||||
})
|
||||
|
||||
return &models.Remote{
|
||||
Name: goGitRemote.Config().Name,
|
||||
Urls: goGitRemote.Config().URLs,
|
||||
Branches: branches,
|
||||
}
|
||||
})
|
||||
|
||||
// now lets sort our remotes by name alphabetically
|
||||
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
|
||||
// we want origin at the top because we'll be most likely to want it
|
||||
if a.Name == "origin" {
|
||||
return true
|
||||
}
|
||||
if b.Name == "origin" {
|
||||
return false
|
||||
}
|
||||
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
|
||||
})
|
||||
|
||||
return remotes, nil
|
||||
}
|
||||
@@ -2,20 +2,20 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
type StashCommands struct {
|
||||
*GitCommon
|
||||
fileLoader *loaders.FileLoader
|
||||
fileLoader *FileLoader
|
||||
workingTree *WorkingTreeCommands
|
||||
}
|
||||
|
||||
func NewStashCommands(
|
||||
gitCommon *GitCommon,
|
||||
fileLoader *loaders.FileLoader,
|
||||
fileLoader *FileLoader,
|
||||
workingTree *WorkingTreeCommands,
|
||||
) *StashCommands {
|
||||
return &StashCommands{
|
||||
@@ -46,8 +46,32 @@ func (self *StashCommands) Save(message string) error {
|
||||
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int) oscommands.ICmdObj {
|
||||
cmdStr := fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", self.UserConfig.Git.Paging.ColorArg, self.UserConfig.Git.DiffContextSize, index)
|
||||
func (self *StashCommands) Store(sha string, message string) error {
|
||||
trimmedMessage := strings.Trim(message, " \t")
|
||||
if len(trimmedMessage) > 0 {
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s -m %s", self.cmd.Quote(sha), self.cmd.Quote(trimmedMessage))).Run()
|
||||
}
|
||||
return self.cmd.New(fmt.Sprintf("git stash store %s", self.cmd.Quote(sha))).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Sha(index int) (string, error) {
|
||||
sha, _, err := self.cmd.New(fmt.Sprintf("git rev-parse refs/stash@{%d}", index)).DontLog().RunWithOutputs()
|
||||
return strings.Trim(sha, "\r\n"), err
|
||||
}
|
||||
|
||||
func (self *StashCommands) ShowStashEntryCmdObj(index int, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
ignoreWhitespaceFlag := ""
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceFlag = " --ignore-all-space"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf(
|
||||
"git stash show -p --stat --color=%s --unified=%d%s stash@{%d}",
|
||||
self.UserConfig.Git.Paging.ColorArg,
|
||||
self.UserConfig.Git.DiffContextSize,
|
||||
ignoreWhitespaceFlag,
|
||||
index,
|
||||
)
|
||||
|
||||
return self.cmd.New(cmdStr).DontLog()
|
||||
}
|
||||
@@ -97,7 +121,7 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
// 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 := self.fileLoader.
|
||||
GetStatusFiles(loaders.GetStatusFileOptions{})
|
||||
GetStatusFiles(GetStatusFileOptions{})
|
||||
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
@@ -109,3 +133,25 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run()
|
||||
}
|
||||
|
||||
func (self *StashCommands) Rename(index int, message string) error {
|
||||
sha, err := self.Sha(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := self.Drop(index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = self.Store(sha, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type StashLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewStashLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *StashLoader {
|
||||
return &StashLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
if filterPath == "" {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return self.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 self.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = self.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 (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list -z --pretty='%gs'").DontLog().RunWithOutput()
|
||||
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
|
||||
return self.stashEntryFromLine(line, index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
return &models.StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedStashEntries []*models.StashEntry
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git stash list -z --pretty='%gs'`, "", nil),
|
||||
[]*models.StashEntry{},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git stash list -z --pretty='%gs'`,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\x00WIP on master: bb86a3f update github template\x00",
|
||||
nil,
|
||||
),
|
||||
[]*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
|
||||
|
||||
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func TestStashDrop(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "", nil)
|
||||
ExpectGitArgs([]string{"stash", "drop", "stash@{1}"}, "Dropped refs/stash@{1} (98e9cca532c37c766107093010c72e26f2c24c04)\n", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.Drop(1))
|
||||
@@ -44,26 +44,89 @@ func TestStashSave(t *testing.T) {
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
func TestStashStore(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
contextSize int
|
||||
expected string
|
||||
testName string
|
||||
sha string
|
||||
message string
|
||||
expected []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
|
||||
testName: "Non-empty message",
|
||||
sha: "0123456789abcdef",
|
||||
message: "New stash name",
|
||||
expected: []string{"stash", "store", "0123456789abcdef", "-m", "New stash name"},
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
index: 5,
|
||||
contextSize: 77,
|
||||
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
|
||||
testName: "Empty message",
|
||||
sha: "0123456789abcdef",
|
||||
message: "",
|
||||
expected: []string{"stash", "store", "0123456789abcdef"},
|
||||
},
|
||||
{
|
||||
testName: "Space message",
|
||||
sha: "0123456789abcdef",
|
||||
message: " ",
|
||||
expected: []string{"stash", "store", "0123456789abcdef"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expected, "", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
assert.NoError(t, instance.Store(s.sha, s.message))
|
||||
runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashSha(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"rev-parse", "refs/stash@{5}"}, "14d94495194651adfd5f070590df566c11d28243\n", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
sha, err := instance.Sha(5)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "14d94495194651adfd5f070590df566c11d28243", sha)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
contextSize int
|
||||
ignoreWhitespace bool
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: false,
|
||||
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
index: 5,
|
||||
contextSize: 77,
|
||||
ignoreWhitespace: false,
|
||||
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
|
||||
},
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
ignoreWhitespace: true,
|
||||
expected: "git stash show -p --stat --color=always --unified=3 --ignore-all-space stash@{5}",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -74,8 +137,55 @@ func TestStashStashEntryCmdObj(t *testing.T) {
|
||||
userConfig.Git.DiffContextSize = s.contextSize
|
||||
instance := buildStashCommands(commonDeps{userConfig: userConfig})
|
||||
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index).ToString()
|
||||
cmdStr := instance.ShowStashEntryCmdObj(s.index, s.ignoreWhitespace).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStashRename(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
message string
|
||||
expectedShaCmd []string
|
||||
shaResult string
|
||||
expectedDropCmd []string
|
||||
expectedStoreCmd []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 3,
|
||||
message: "New message",
|
||||
expectedShaCmd: []string{"rev-parse", "refs/stash@{3}"},
|
||||
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
|
||||
expectedDropCmd: []string{"stash", "drop", "stash@{3}"},
|
||||
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd", "-m", "New message"},
|
||||
},
|
||||
{
|
||||
testName: "Empty message",
|
||||
index: 4,
|
||||
message: "",
|
||||
expectedShaCmd: []string{"rev-parse", "refs/stash@{4}"},
|
||||
shaResult: "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd\n",
|
||||
expectedDropCmd: []string{"stash", "drop", "stash@{4}"},
|
||||
expectedStoreCmd: []string{"stash", "store", "f0d0f20f2f61ffd6d6bfe0752deffa38845a3edd"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs(s.expectedShaCmd, s.shaResult, nil).
|
||||
ExpectGitArgs(s.expectedDropCmd, "", nil).
|
||||
ExpectGitArgs(s.expectedStoreCmd, "", nil)
|
||||
instance := buildStashCommands(commonDeps{runner: runner})
|
||||
|
||||
err := instance.Rename(s.index, s.message)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package git_commands
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
)
|
||||
|
||||
@@ -49,13 +51,21 @@ func (self *StatusCommands) WorkingTreeState() enums.RebaseMode {
|
||||
return enums.REBASE_MODE_NONE
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsBareRepo() (bool, error) {
|
||||
return IsBareRepo(self.os)
|
||||
}
|
||||
|
||||
func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
|
||||
res, err := osCommand.Cmd.New("git rev-parse --is-bare-repository").DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The command returns output with a newline, so we need to strip
|
||||
return strconv.ParseBool(strings.TrimSpace(res))
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (self *StatusCommands) IsInMergeState() (bool, error) {
|
||||
return self.os.FileExists(filepath.Join(self.dotGitDir, "MERGE_HEAD"))
|
||||
}
|
||||
|
||||
func (self *StatusCommands) IsBareRepo() bool {
|
||||
// note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git
|
||||
_, err := self.repo.Worktree()
|
||||
return err == gogit.ErrIsBareRepository
|
||||
}
|
||||
|
||||
@@ -14,12 +14,20 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateLightweight(tagName string, commitSha string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), commitSha)).Run()
|
||||
func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
|
||||
if len(ref) > 0 {
|
||||
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), self.cmd.Quote(ref))).Run()
|
||||
} else {
|
||||
return self.cmd.New(fmt.Sprintf("git tag -- %s", self.cmd.Quote(tagName))).Run()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) CreateAnnotated(tagName, commitSha, msg string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, self.cmd.Quote(msg))).Run()
|
||||
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
|
||||
if len(ref) > 0 {
|
||||
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(ref), self.cmd.Quote(msg))).Run()
|
||||
} else {
|
||||
return self.cmd.New(fmt.Sprintf("git tag %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(msg))).Run()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagCommands) Delete(tagName string) error {
|
||||
@@ -27,5 +35,5 @@ func (self *TagCommands) Delete(tagName string) error {
|
||||
}
|
||||
|
||||
func (self *TagCommands) Push(remoteName string, tagName string) error {
|
||||
return self.cmd.New(fmt.Sprintf("git push %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
return self.cmd.New(fmt.Sprintf("git push %s tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type TagLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewTagLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *TagLoader {
|
||||
return &TagLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
tagsOutput, err := self.cmd.New(`git tag --list -n --sort=-creatordate`).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
split := utils.SplitLines(tagsOutput)
|
||||
|
||||
lineRegex := regexp.MustCompile(`^([^\s]+)(\s+)?(.*)$`)
|
||||
|
||||
tags := slices.Map(split, func(line string) *models.Tag {
|
||||
matches := lineRegex.FindStringSubmatch(line)
|
||||
tagName := matches[1]
|
||||
message := ""
|
||||
if len(matches) > 3 {
|
||||
message = matches[3]
|
||||
}
|
||||
|
||||
return &models.Tag{
|
||||
Name: tagName,
|
||||
Message: message,
|
||||
}
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const tagsOutput = `tag1 this is my message
|
||||
tag2
|
||||
tag3 this is my other message
|
||||
`
|
||||
|
||||
func TestGetTags(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedTags []*models.Tag
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no tags if there are none",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list -n --sort=-creatordate`, "", nil),
|
||||
expectedTags: []*models.Tag{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return tags if present",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list -n --sort=-creatordate`, tagsOutput, nil),
|
||||
expectedTags: []*models.Tag{
|
||||
{Name: "tag1", Message: "this is my message"},
|
||||
{Name: "tag2", Message: ""},
|
||||
{Name: "tag3", Message: "this is my other message"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
tags, err := loader.GetTags()
|
||||
|
||||
assert.Equal(t, scenario.expectedTags, tags)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
type GitVersion struct {
|
||||
Major, Minor, Patch int
|
||||
Additional string
|
||||
}
|
||||
|
||||
func GetGitVersion(osCommand *oscommands.OSCommand) (*GitVersion, error) {
|
||||
versionStr, _, err := osCommand.Cmd.New("git --version").RunWithOutputs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version, err := ParseGitVersion(versionStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func ParseGitVersion(versionStr string) (*GitVersion, error) {
|
||||
// versionStr should be something like:
|
||||
// git version 2.39.0
|
||||
// git version 2.37.1 (Apple Git-137.1)
|
||||
re := regexp.MustCompile(`[^\d]*(\d+)(\.\d+)?(\.\d+)?(.*)`)
|
||||
matches := re.FindStringSubmatch(versionStr)
|
||||
|
||||
if len(matches) < 5 {
|
||||
return nil, errors.New("unexpected git version format: " + versionStr)
|
||||
}
|
||||
|
||||
v := &GitVersion{}
|
||||
var err error
|
||||
|
||||
if v.Major, err = strconv.Atoi(matches[1]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(matches[2]) > 1 {
|
||||
if v.Minor, err = strconv.Atoi(matches[2][1:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(matches[3]) > 1 {
|
||||
if v.Patch, err = strconv.Atoi(matches[3][1:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
v.Additional = strings.Trim(matches[4], " \r\n")
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (v *GitVersion) IsOlderThan(major, minor, patch int) bool {
|
||||
actual := v.Major*1000*1000 + v.Minor*1000 + v.Patch
|
||||
required := major*1000*1000 + minor*1000 + patch
|
||||
return actual < required
|
||||
}
|
||||
|
||||
func (v *GitVersion) IsOlderThanVersion(version *GitVersion) bool {
|
||||
return v.IsOlderThan(version.Major, version.Minor, version.Patch)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package git_commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseGitVersion(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
input string
|
||||
expected GitVersion
|
||||
}{
|
||||
{
|
||||
input: "git version 2.39.0",
|
||||
expected: GitVersion{Major: 2, Minor: 39, Patch: 0, Additional: ""},
|
||||
},
|
||||
{
|
||||
input: "git version 2.37.1 (Apple Git-137.1)",
|
||||
expected: GitVersion{Major: 2, Minor: 37, Patch: 1, Additional: "(Apple Git-137.1)"},
|
||||
},
|
||||
{
|
||||
input: "git version 2.37 (Apple Git-137.1)",
|
||||
expected: GitVersion{Major: 2, Minor: 37, Patch: 0, Additional: "(Apple Git-137.1)"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
actual, err := ParseGitVersion(s.input)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, actual)
|
||||
assert.Equal(t, s.expected.Major, actual.Major)
|
||||
assert.Equal(t, s.expected.Minor, actual.Minor)
|
||||
assert.Equal(t, s.expected.Patch, actual.Patch)
|
||||
assert.Equal(t, s.expected.Additional, actual.Additional)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitVersionIsOlderThan(t *testing.T) {
|
||||
assert.False(t, (&GitVersion{2, 0, 0, ""}).IsOlderThan(1, 99, 99))
|
||||
assert.False(t, (&GitVersion{2, 0, 0, ""}).IsOlderThan(2, 0, 0))
|
||||
assert.False(t, (&GitVersion{2, 1, 0, ""}).IsOlderThan(2, 0, 9))
|
||||
|
||||
assert.True(t, (&GitVersion{2, 0, 1, ""}).IsOlderThan(2, 1, 0))
|
||||
assert.True(t, (&GitVersion{2, 0, 1, ""}).IsOlderThan(3, 0, 0))
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -18,13 +17,13 @@ import (
|
||||
type WorkingTreeCommands struct {
|
||||
*GitCommon
|
||||
submodule *SubmoduleCommands
|
||||
fileLoader *loaders.FileLoader
|
||||
fileLoader *FileLoader
|
||||
}
|
||||
|
||||
func NewWorkingTreeCommands(
|
||||
gitCommon *GitCommon,
|
||||
submodule *SubmoduleCommands,
|
||||
fileLoader *loaders.FileLoader,
|
||||
fileLoader *FileLoader,
|
||||
) *WorkingTreeCommands {
|
||||
return &WorkingTreeCommands{
|
||||
GitCommon: gitCommon,
|
||||
@@ -90,7 +89,7 @@ func (self *WorkingTreeCommands) BeforeAndAfterFileForRename(file *models.File)
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file.
|
||||
|
||||
filesWithoutRenames := self.fileLoader.GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
|
||||
filesWithoutRenames := self.fileLoader.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
|
||||
var beforeFile *models.File
|
||||
var afterFile *models.File
|
||||
@@ -241,7 +240,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain
|
||||
if cached {
|
||||
cachedArg = " --cached"
|
||||
}
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() {
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
@@ -288,11 +287,15 @@ func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error
|
||||
|
||||
// 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 (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
|
||||
func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool,
|
||||
ignoreWhitespace bool,
|
||||
) (string, error) {
|
||||
return self.ShowFileDiffCmdObj(from, to, reverse, fileName, plain, ignoreWhitespace).RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
|
||||
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool,
|
||||
ignoreWhitespace bool,
|
||||
) oscommands.ICmdObj {
|
||||
colorArg := self.UserConfig.Git.Paging.ColorArg
|
||||
contextSize := self.UserConfig.Git.DiffContextSize
|
||||
if plain {
|
||||
@@ -304,11 +307,16 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve
|
||||
reverseFlag = " -R"
|
||||
}
|
||||
|
||||
ignoreWhitespaceFlag := ""
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceFlag = " --ignore-all-space"
|
||||
}
|
||||
|
||||
return self.cmd.
|
||||
New(
|
||||
fmt.Sprintf(
|
||||
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s -- %s",
|
||||
contextSize, colorArg, pad(from), pad(to), reverseFlag, self.cmd.Quote(fileName)),
|
||||
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s%s -- %s",
|
||||
contextSize, colorArg, pad(from), pad(to), reverseFlag, ignoreWhitespaceFlag, self.cmd.Quote(fileName)),
|
||||
).
|
||||
DontLog()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@@ -323,38 +323,52 @@ func TestWorkingTreeDiff(t *testing.T) {
|
||||
|
||||
func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
plain bool
|
||||
contextSize int
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
plain bool
|
||||
ignoreWhitespace bool
|
||||
contextSize int
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
}
|
||||
|
||||
const expectedResult = "pretend this is an actual git diff"
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
contextSize: 3,
|
||||
testName: "Default case",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
contextSize: 123,
|
||||
testName: "Show diff with custom context size",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: false,
|
||||
contextSize: 123,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=123 --no-renames --color=always 1234567890 0987654321 -- "test.txt"`, expectedResult, nil),
|
||||
},
|
||||
{
|
||||
testName: "Default case (ignore whitespace)",
|
||||
from: "1234567890",
|
||||
to: "0987654321",
|
||||
reverse: false,
|
||||
plain: false,
|
||||
ignoreWhitespace: true,
|
||||
contextSize: 3,
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git diff --submodule --no-ext-diff --unified=3 --no-renames --color=always 1234567890 0987654321 --ignore-all-space -- "test.txt"`, expectedResult, nil),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -365,7 +379,7 @@ func TestWorkingTreeShowFileDiff(t *testing.T) {
|
||||
|
||||
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner, userConfig: userConfig})
|
||||
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain)
|
||||
result, err := instance.ShowFileDiff(s.from, s.to, s.reverse, "test.txt", s.plain, s.ignoreWhitespace)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, result)
|
||||
s.runner.CheckForMissingCalls()
|
||||
@@ -432,7 +446,7 @@ func TestWorkingTreeApplyPatch(t *testing.T) {
|
||||
|
||||
filename := matches[1]
|
||||
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
content, err := os.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
@@ -3,7 +3,7 @@ package git_config
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
func runGitConfigCmd(cmd *exec.Cmd) (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = ioutil.Discard
|
||||
cmd.Stderr = io.Discard
|
||||
|
||||
err := cmd.Run()
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
|
||||
@@ -3,15 +3,16 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -218,9 +219,10 @@ func TestNewGitCommand(t *testing.T) {
|
||||
s.setup()
|
||||
s.test(
|
||||
NewGitCommand(utils.NewDummyCommon(),
|
||||
&git_commands.GitVersion{},
|
||||
oscommands.NewDummyOSCommand(),
|
||||
git_config.NewFakeGitConfig(nil),
|
||||
&sync.Mutex{},
|
||||
&deadlock.Mutex{},
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package hosting_service
|
||||
// if you want to make a custom regex for a given service feel free to test it out
|
||||
// at regoio.herokuapp.com
|
||||
var defaultUrlRegexStrings = []string{
|
||||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^(?:https?|ssh)://[^/]+/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^.*?@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
}
|
||||
var defaultRepoURLTemplate = "https://{{.webDomain}}/{{.owner}}/{{.repo}}"
|
||||
|
||||
@@ -24,8 +24,11 @@ var bitbucketServiceDef = ServiceDefinition{
|
||||
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
|
||||
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
|
||||
commitURL: "/commits/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
regexStrings: []string{
|
||||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^.*@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
},
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var gitLabServiceDef = ServiceDefinition{
|
||||
@@ -61,12 +64,22 @@ var bitbucketServerServiceDef = ServiceDefinition{
|
||||
repoURLTemplate: "https://{{.webDomain}}/projects/{{.project}}/repos/{{.repo}}",
|
||||
}
|
||||
|
||||
var giteaServiceDef = ServiceDefinition{
|
||||
provider: "gitea",
|
||||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
repoURLTemplate: defaultRepoURLTemplate,
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{
|
||||
githubServiceDef,
|
||||
bitbucketServiceDef,
|
||||
gitLabServiceDef,
|
||||
azdoServiceDef,
|
||||
bitbucketServerServiceDef,
|
||||
giteaServiceDef,
|
||||
}
|
||||
|
||||
var defaultServiceDomains = []ServiceDomain{
|
||||
@@ -90,4 +103,9 @@ var defaultServiceDomains = []ServiceDomain{
|
||||
gitDomain: "dev.azure.com",
|
||||
webDomain: "dev.azure.com",
|
||||
},
|
||||
{
|
||||
serviceDefinition: giteaServiceDef,
|
||||
gitDomain: "try.gitea.io",
|
||||
webDomain: "try.gitea.io",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
)
|
||||
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, gitea, etc.
|
||||
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
|
||||
// and this package's responsibility is to determine which service you're using based on the remote URL,
|
||||
// and then which URL you need for whatever use case you have.
|
||||
|
||||
@@ -3,8 +3,8 @@ package hosting_service
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/fakes"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -47,6 +47,15 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with https remote url",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "https://github.com/peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
@@ -77,6 +86,26 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch (different git username)",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "ssh://org-12345@github.com:peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with https remote url with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "https://github.com/peter/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature%2Foperations...feature%2Fsum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
@@ -95,6 +124,15 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with https remote url in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
@@ -115,6 +153,25 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with https remote url with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "https://gitlab.com/peter/public/calculator.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature%2Fcommit-ui&merge_request[target_branch]=epic%2Fui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with a custom SSH username",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "john@bitbucket.org:johndoe/social_network.git",
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Azure DevOps (SSH)",
|
||||
from: "feature/new",
|
||||
@@ -207,6 +264,60 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.Equal(t, "https://mycompany.bitbucket.com/projects/myproject/repos/myrepo/pull-requests?create&targetBranch=dev&sourceBranch=feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (SSH) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "ssh://git@mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP)",
|
||||
from: "feature/new",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on Gitea Server (HTTP) with specific target",
|
||||
from: "feature/new",
|
||||
to: "dev",
|
||||
remoteUrl: "https://mycompany.gitea.io/myproject/myrepo.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a gitea server URL
|
||||
"mycompany.gitea.io": "gitea:mycompany.gitea.io",
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://mycompany.gitea.io/myproject/myrepo/compare/dev...feature%2Fnew", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
@@ -253,7 +364,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature%2Fprofile-page&t=1", url)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer"},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab, azuredevops, bitbucketServer, gitea"},
|
||||
},
|
||||
{
|
||||
testName: "Escapes reserved URL characters in from branch name",
|
||||
@@ -281,7 +392,7 @@ func TestGetPullRequestURL(t *testing.T) {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
log := &test.FakeFieldLogger{}
|
||||
log := &fakes.FakeFieldLogger{}
|
||||
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
|
||||
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
|
||||
log.AssertErrors(t, s.expectedLoggedErrors)
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/go-git/v5/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
type BranchLoaderConfigCommands interface {
|
||||
Branches() (map[string]*config.Branch, error)
|
||||
}
|
||||
|
||||
// BranchLoader returns a list of Branch objects for the current repo
|
||||
type BranchLoader struct {
|
||||
*common.Common
|
||||
getRawBranches func() (string, error)
|
||||
getCurrentBranchName func() (string, string, error)
|
||||
config BranchLoaderConfigCommands
|
||||
}
|
||||
|
||||
func NewBranchLoader(
|
||||
cmn *common.Common,
|
||||
getRawBranches func() (string, error),
|
||||
getCurrentBranchName func() (string, string, error),
|
||||
config BranchLoaderConfigCommands,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
Common: cmn,
|
||||
getRawBranches: getRawBranches,
|
||||
getCurrentBranchName: getCurrentBranchName,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Load the list of branches for the current repo
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
|
||||
branches := self.obtainBranches()
|
||||
|
||||
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
||||
|
||||
// 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 = slices.Remove(branches, j)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = slices.Prepend(branches, branchesWithRecency...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = slices.Move(branches, i, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
branches = slices.Prepend(branches, &models.Branch{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"})
|
||||
}
|
||||
|
||||
configBranches, err := self.config.Branches()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
match := configBranches[branch.Name]
|
||||
if match != nil {
|
||||
branch.UpstreamRemote = match.Remote
|
||||
branch.UpstreamBranch = match.Merge.Short()
|
||||
}
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
output, err := self.getRawBranches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
|
||||
return slices.FilterMap(outputLines, func(line string) (*models.Branch, bool) {
|
||||
if line == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
split := strings.Split(line, "\x00")
|
||||
if len(split) != 4 {
|
||||
// Ignore line if it isn't separated into 4 parts
|
||||
// This is probably a warning message, for more info see:
|
||||
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return obtainBranch(split), true
|
||||
})
|
||||
}
|
||||
|
||||
// Obtain branch information from parsed line output of getRawBranches()
|
||||
// split contains the '|' separated tokens in the line of output
|
||||
func obtainBranch(split []string) *models.Branch {
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
// if we're here then it means we do not have a local version of the remote.
|
||||
// The branch might still be tracking a remote though, we just don't know
|
||||
// how many commits ahead/behind it is
|
||||
return branch
|
||||
}
|
||||
|
||||
track := split[3]
|
||||
if track == "[gone]" {
|
||||
branch.UpstreamGone = true
|
||||
} else {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
return branch
|
||||
}
|
||||
|
||||
// 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 (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
|
||||
foundBranches := set.New[string]()
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
|
||||
|
||||
for _, commit := range reflogCommits {
|
||||
match := re.FindStringSubmatch(commit.Name)
|
||||
if len(match) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranches.Includes(branchName) {
|
||||
foundBranches.Add(branchName)
|
||||
reflogBranches = append(reflogBranches, &models.Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return reflogBranches
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package loaders
|
||||
|
||||
// "*|feat/detect-purge|origin/feat/detect-purge|[ahead 1]"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestObtainBanch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
input []string
|
||||
expectedBranch *models.Branch
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "TrimHeads",
|
||||
input: []string{"", "heads/a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "NoUpstream",
|
||||
input: []string{"", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "IsHead",
|
||||
input: []string{"*", "a_branch", "", ""},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "?", Pullables: "?", Head: true},
|
||||
},
|
||||
{
|
||||
testName: "IsBehindAndAhead",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", Pushables: "3", Pullables: "2", Head: false},
|
||||
},
|
||||
{
|
||||
testName: "RemoteBranchIsGone",
|
||||
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]"},
|
||||
expectedBranch: &models.Branch{Name: "a_branch", UpstreamGone: true, Pushables: "?", Pullables: "?", Head: false},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
branch := obtainBranch(s.input)
|
||||
assert.EqualValues(t, s.expectedBranch, branch)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type CommitFileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
|
||||
return &CommitFileLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "MM\x00file1\x00MU\x00file2\x00AA\x00file3\x00"
|
||||
// so we need to split it by the null character and then map each status-name pair to a commit file
|
||||
func getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
if len(lines) == 1 {
|
||||
return []*models.CommitFile{}
|
||||
}
|
||||
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
return slices.Map(lo.Chunk(lines, 2), func(chunk []string) *models.CommitFile {
|
||||
return &models.CommitFile{
|
||||
ChangeStatus: chunk[0],
|
||||
Name: chunk[1],
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCommitFilesFromFilenames(t *testing.T) {
|
||||
tests := []struct {
|
||||
testName string
|
||||
input string
|
||||
output []*models.CommitFile
|
||||
}{
|
||||
{
|
||||
testName: "no files",
|
||||
input: "",
|
||||
output: []*models.CommitFile{},
|
||||
},
|
||||
{
|
||||
testName: "one file",
|
||||
input: "MM\x00Myfile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "two files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "three files",
|
||||
input: "MM\x00Myfile\x00M \x00MyOtherFile\x00 M\x00YetAnother\x00",
|
||||
output: []*models.CommitFile{
|
||||
{
|
||||
Name: "Myfile",
|
||||
ChangeStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "MyOtherFile",
|
||||
ChangeStatus: "M ",
|
||||
},
|
||||
{
|
||||
Name: "YetAnother",
|
||||
ChangeStatus: " M",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
result := getCommitFilesFromFilenames(test.input)
|
||||
assert.Equal(t, test.output, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// CommitLoader returns a list of Commit objects for the current repo
|
||||
type CommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
|
||||
getCurrentBranchName func() (string, string, error)
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
}
|
||||
|
||||
// making our dependencies explicit for the sake of easier testing
|
||||
func NewCommitLoader(
|
||||
cmn *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dotGitDir string,
|
||||
getCurrentBranchName func() (string, string, error),
|
||||
getRebaseMode func() (enums.RebaseMode, error),
|
||||
) *CommitLoader {
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getCurrentBranchName: getCurrentBranchName,
|
||||
getRebaseMode: getRebaseMode,
|
||||
readFile: ioutil.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
dotGitDir: dotGitDir,
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
// determines if we show the whole git graph i.e. pass the '--all' flag
|
||||
All bool
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
passedFirstPushedCommit := false
|
||||
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
|
||||
if err != nil {
|
||||
// must have no upstream branch so we'll consider everything as pushed
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
|
||||
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.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 len(commits) == 0 {
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
if rebaseMode != enums.REBASE_MODE_NONE {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (self *CommitLoader) 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 := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode == enums.REBASE_MODE_NONE {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 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 (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
||||
split := strings.SplitN(line, "\x00", 7)
|
||||
|
||||
sha := split[0]
|
||||
unixTimestamp := split[1]
|
||||
authorName := split[2]
|
||||
authorEmail := split[3]
|
||||
extraInfo := strings.TrimSpace(split[4])
|
||||
parentHashes := split[5]
|
||||
message := split[6]
|
||||
|
||||
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)
|
||||
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
return &models.Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
AuthorName: authorName,
|
||||
AuthorEmail: authorEmail,
|
||||
Parents: parents,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
commits, err := self.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := slices.Map(commits, func(commit *models.Commit) string {
|
||||
return commit.Sha
|
||||
})
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
),
|
||||
).DontLog()
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case enums.REBASE_MODE_MERGING:
|
||||
return self.getNormalRebasingCommits()
|
||||
case enums.REBASE_MODE_INTERACTIVE:
|
||||
return self.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := self.readFile(filepath.Join(self.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 = self.walkFiles(filepath.Join(self.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 := self.readFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := string(bytesContent)
|
||||
commit := self.commitFromPatch(content)
|
||||
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 (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
|
||||
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
|
||||
if err != nil {
|
||||
self.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{}
|
||||
|
||||
todos, err := todo.Parse(bytes.NewBuffer(bytesContent))
|
||||
if err != nil {
|
||||
self.Log.Error(fmt.Sprintf("error occurred while parsing git-rebase-todo file: %s", err.Error()))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, t := range todos {
|
||||
if t.Commit == "" {
|
||||
// Command does not have a commit associated, skip
|
||||
continue
|
||||
}
|
||||
commits = slices.Prepend(commits, &models.Commit{
|
||||
Sha: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: "rebasing",
|
||||
Action: t.Command.String(),
|
||||
})
|
||||
}
|
||||
|
||||
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 (self *CommitLoader) commitFromPatch(content string) *models.Commit {
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
ancestor, err := self.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 (self *CommitLoader) getMergeBase(refName string) (string, error) {
|
||||
currentBranch, _, err := self.getCurrentBranchName()
|
||||
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, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).DontLog().RunWithOutput()
|
||||
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 (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := self.cmd.
|
||||
New(
|
||||
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
|
||||
).
|
||||
DontLog().
|
||||
RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
limitFlag := ""
|
||||
if opts.Limit {
|
||||
limitFlag = " -300"
|
||||
}
|
||||
|
||||
filterFlag := ""
|
||||
if opts.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
config := self.UserConfig.Git.Log
|
||||
|
||||
orderFlag := "--" + config.Order
|
||||
allFlag := ""
|
||||
if opts.All {
|
||||
allFlag = " --all"
|
||||
}
|
||||
|
||||
return self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git -c log.showSignature=false log %s %s %s --oneline %s%s --abbrev=%d%s",
|
||||
self.cmd.Quote(opts.RefName),
|
||||
orderFlag,
|
||||
allFlag,
|
||||
prettyFormat,
|
||||
limitFlag,
|
||||
40,
|
||||
filterFlag,
|
||||
),
|
||||
).DontLog()
|
||||
}
|
||||
|
||||
var prettyFormat = fmt.Sprintf(
|
||||
"--pretty=format:\"%%H%s%%at%s%%aN%s%%ae%s%%d%s%%p%s%%s\"",
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
NULL_CODE,
|
||||
)
|
||||
|
||||
const NULL_CODE = "%x00"
|
||||
@@ -1,206 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var commitsOutput = strings.Replace(`0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield|jessedduffield@gmail.com| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield|jessedduffield@gmail.com| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield|jessedduffield@gmail.com||d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield|jessedduffield@gmail.com||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield|jessedduffield@gmail.com||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield|jessedduffield@gmail.com||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield|jessedduffield@gmail.com||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield|jessedduffield@gmail.com||985fe482e806b172aea4|refactoring the config struct`, "|", "\x00", -1)
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
rebaseMode enums.RebaseMode
|
||||
currentBranchName string
|
||||
opts GetCommitsOptions
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git -c log.showSignature=false log "HEAD" --topo-order --oneline --pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s" --abbrev=40`, commitsOutput, nil).
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: "unpushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
|
||||
Name: "fix logging",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640824515,
|
||||
Parents: []string{
|
||||
"e94e8fc5b6fab4cb755f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
|
||||
Name: "refactor",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640823749,
|
||||
Parents: []string{
|
||||
"d8084cd558925eb7c9c3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821426,
|
||||
Parents: []string{
|
||||
"65f910ebd85283b5cce9",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640821275,
|
||||
Parents: []string{
|
||||
"26c07b1ab33860a1a759",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640750752,
|
||||
Parents: []string{
|
||||
"3d4470a6c072208722e5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640748818,
|
||||
Parents: []string{
|
||||
"053a66a7be3da43aacdc",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
|
||||
Name: "refactoring the config struct",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
AuthorName: "Jesse Duffield",
|
||||
AuthorEmail: "jessedduffield@gmail.com",
|
||||
UnixTimestamp: 1640739815,
|
||||
Parents: []string{
|
||||
"985fe482e806b172aea4",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &CommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
getCurrentBranchName: func() (string, string, error) {
|
||||
return scenario.currentBranchName, scenario.currentBranchName, nil
|
||||
},
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
walkFiles: func(root string, fn filepath.WalkFunc) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
commits, err := builder.GetCommits(scenario.opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type FileLoaderConfig interface {
|
||||
GetShowUntrackedFiles() string
|
||||
}
|
||||
|
||||
type FileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
config FileLoaderConfig
|
||||
getFileType func(string) string
|
||||
}
|
||||
|
||||
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, config FileLoaderConfig) *FileLoader {
|
||||
return &FileLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
getFileType: oscommands.FileType,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := self.config.GetShowUntrackedFiles()
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
self.Log.Error(err)
|
||||
}
|
||||
files := []*models.File{}
|
||||
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := change[1:2]
|
||||
untracked := lo.Contains([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := lo.Contains([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, change)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, change)
|
||||
|
||||
file := &models.File{
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: self.getFileType(status.Name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the file status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
type FileStatus struct {
|
||||
StatusString string
|
||||
Change string // ??, MM, AM, ...
|
||||
Name string
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = " --no-renames"
|
||||
}
|
||||
|
||||
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
response := []FileStatus{}
|
||||
|
||||
for i := 0; i < len(splitLines); i++ {
|
||||
original := splitLines[i]
|
||||
|
||||
if len(original) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
status := FileStatus{
|
||||
StatusString: original,
|
||||
Change: original[:2],
|
||||
Name: original[3:],
|
||||
PreviousName: "",
|
||||
}
|
||||
|
||||
if strings.HasPrefix(status.Change, "R") {
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
status.PreviousName = splitLines[i+1]
|
||||
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
|
||||
i++
|
||||
}
|
||||
|
||||
response = append(response, status)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedFiles []*models.File
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
|
||||
[]*models.File{},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM a\nb.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "after1.txt",
|
||||
PreviousName: "before1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "R before1.txt -> after1.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "R ",
|
||||
},
|
||||
{
|
||||
Name: "after2.txt",
|
||||
PreviousName: "before2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "RM before2.txt -> after2.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
`?? a -> b.txt`,
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "a -> b.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? a -> b.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := &FileLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: cmd,
|
||||
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
|
||||
getFileType: func(string) string { return "file" },
|
||||
}
|
||||
|
||||
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type FakeFileLoaderConfig struct {
|
||||
showUntrackedFiles string
|
||||
}
|
||||
|
||||
func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
|
||||
return self.showUntrackedFiles
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type ReflogCommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
|
||||
return &ReflogCommitLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
commits := make([]*models.Commit, 0)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmdObj := self.cmd.New(fmt.Sprintf(`git -c log.showSignature=false log -g --abbrev=40 --format="%s"%s`, "%h%x00%ct%x00%gs%x00%p", filterPathArg)).DontLog()
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
fields := strings.SplitN(line, "\x00", 4)
|
||||
if len(fields) <= 3 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||
|
||||
parentHashes := fields[3]
|
||||
parents := []string{}
|
||||
if len(parentHashes) > 0 {
|
||||
parents = strings.Split(parentHashes, " ")
|
||||
}
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
Parents: parents,
|
||||
}
|
||||
|
||||
// note that the unix timestamp here is the timestamp of the COMMIT, not the reflog entry itself,
|
||||
// so two consecutive reflog entries may have both the same SHA and therefore same timestamp.
|
||||
// We use the reflog message to disambiguate, and fingers crossed that we never see the same of those
|
||||
// twice in a row. Reason being that it would mean we'd be erroneously exiting early.
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp && commit.Name == lastReflogCommit.Name {
|
||||
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
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sanity-io/litter"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var reflogOutput = strings.Replace(`c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from B to A|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from A to B|51baa8c1
|
||||
c3c4b66b64c97ffeecde|1643150483|checkout: moving from master to A|51baa8c1
|
||||
f4ddf2f0d4be4ccc7efa|1643149435|checkout: moving from A to master|51baa8c1
|
||||
`, "|", "\x00", -1)
|
||||
|
||||
func TestGetReflogCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
lastReflogCommit *models.Commit
|
||||
filterPath string
|
||||
expectedCommits []*models.Commit
|
||||
expectedOnlyObtainedNew bool
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "no reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "some reflog entries",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from master to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
{
|
||||
Sha: "f4ddf2f0d4be4ccc7efa",
|
||||
Name: "checkout: moving from A to master",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643149435,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "some reflog entries where last commit is given",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "when passing filterPath",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p" --follow -- "path"`, reflogOutput, nil),
|
||||
|
||||
lastReflogCommit: &models.Commit{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from B to A",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
filterPath: "path",
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "c3c4b66b64c97ffeecde",
|
||||
Name: "checkout: moving from A to B",
|
||||
Status: "reflog",
|
||||
UnixTimestamp: 1643150483,
|
||||
Parents: []string{"51baa8c1"},
|
||||
},
|
||||
},
|
||||
expectedOnlyObtainedNew: true,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "when command returns error",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git -c log.showSignature=false log -g --abbrev=40 --format="%h%x00%ct%x00%gs%x00%p"`, "", errors.New("haha")),
|
||||
|
||||
lastReflogCommit: nil,
|
||||
filterPath: "",
|
||||
expectedCommits: nil,
|
||||
expectedOnlyObtainedNew: false,
|
||||
expectedError: errors.New("haha"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &ReflogCommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
commits, onlyObtainednew, err := builder.GetReflogCommits(scenario.lastReflogCommit, scenario.filterPath)
|
||||
assert.Equal(t, scenario.expectedOnlyObtainedNew, onlyObtainednew)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
t.Logf("actual commits: \n%s", litter.Sdump(commits))
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type RemoteLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error)
|
||||
}
|
||||
|
||||
func NewRemoteLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error),
|
||||
) *RemoteLoader {
|
||||
return &RemoteLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
getGoGitRemotes: getGoGitRemotes,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goGitRemotes, err := self.getGoGitRemotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
remotes := slices.Map(goGitRemotes, func(goGitRemote *gogit.Remote) *models.Remote {
|
||||
remoteName := goGitRemote.Config().Name
|
||||
|
||||
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
|
||||
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
|
||||
branches := slices.Map(matches, func(match []string) *models.RemoteBranch {
|
||||
return &models.RemoteBranch{
|
||||
Name: match[1],
|
||||
RemoteName: remoteName,
|
||||
}
|
||||
})
|
||||
|
||||
return &models.Remote{
|
||||
Name: goGitRemote.Config().Name,
|
||||
Urls: goGitRemote.Config().URLs,
|
||||
Branches: branches,
|
||||
}
|
||||
})
|
||||
|
||||
// now lets sort our remotes by name alphabetically
|
||||
slices.SortFunc(remotes, func(a, b *models.Remote) bool {
|
||||
// we want origin at the top because we'll be most likely to want it
|
||||
if a.Name == "origin" {
|
||||
return true
|
||||
}
|
||||
if b.Name == "origin" {
|
||||
return false
|
||||
}
|
||||
return strings.ToLower(a.Name) < strings.ToLower(b.Name)
|
||||
})
|
||||
|
||||
return remotes, nil
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type StashLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewStashLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *StashLoader {
|
||||
return &StashLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
if filterPath == "" {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return self.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 self.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = self.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 (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").DontLog().RunWithOutput()
|
||||
return slices.MapWithIndex(utils.SplitLines(rawString), func(line string, index int) *models.StashEntry {
|
||||
return self.stashEntryFromLine(line, index)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
return &models.StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedStashEntries []*models.StashEntry
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git stash list --pretty='%gs'`, "", nil),
|
||||
[]*models.StashEntry{},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git stash list --pretty='%gs'`,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
|
||||
nil,
|
||||
),
|
||||
[]*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
|
||||
|
||||
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(s.filterPath))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type TagLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewTagLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *TagLoader {
|
||||
return &TagLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
tagsOutput, err := self.cmd.New(`git tag --list --sort=-creatordate`).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
split := utils.SplitLines(tagsOutput)
|
||||
|
||||
tags := slices.Map(split, func(tagName string) *models.Tag {
|
||||
return &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const tagsOutput = `v0.34
|
||||
v0.33
|
||||
v0.32.2
|
||||
v0.32.1
|
||||
v0.32
|
||||
testtag
|
||||
`
|
||||
|
||||
func TestGetTags(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedTags []*models.Tag
|
||||
expectedError error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no tags if there are none",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, "", nil),
|
||||
expectedTags: []*models.Tag{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return tags if present",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git tag --list --sort=-creatordate`, tagsOutput, nil),
|
||||
expectedTags: []*models.Tag{
|
||||
{Name: "v0.34"},
|
||||
{Name: "v0.33"},
|
||||
{Name: "v0.32.2"},
|
||||
{Name: "v0.32.1"},
|
||||
{Name: "v0.32"},
|
||||
{Name: "testtag"},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario := scenario
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
loader := &TagLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
}
|
||||
|
||||
tags, err := loader.GetTags()
|
||||
|
||||
assert.Equal(t, scenario.expectedTags, tags)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
|
||||
scenario.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type Branch struct {
|
||||
Pullables string
|
||||
UpstreamGone bool
|
||||
Head bool
|
||||
DetachedHead bool
|
||||
// if we have a named remote locally this will be the name of that remote e.g.
|
||||
// 'origin' or 'tiwood'. If we don't have the remote locally it'll look like
|
||||
// 'git@github.com:tiwood/lazygit.git'
|
||||
@@ -19,6 +20,9 @@ type Branch struct {
|
||||
}
|
||||
|
||||
func (b *Branch) FullRefName() string {
|
||||
if b.DetachedHead {
|
||||
return b.Name
|
||||
}
|
||||
return "refs/heads/" + b.Name
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,37 @@ package models
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fsmiamoto/git-todo-parser/todo"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Special commit hash for empty tree object
|
||||
const EmptyTreeCommitHash = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
type CommitStatus int
|
||||
|
||||
const (
|
||||
StatusNone CommitStatus = iota
|
||||
StatusUnpushed
|
||||
StatusPushed
|
||||
StatusMerged
|
||||
StatusRebasing
|
||||
StatusSelected
|
||||
StatusReflog
|
||||
)
|
||||
|
||||
const (
|
||||
// Conveniently for us, the todo package starts the enum at 1, and given
|
||||
// that it doesn't have a "none" value, we're setting ours to 0
|
||||
ActionNone todo.TodoCommand = 0
|
||||
)
|
||||
|
||||
// 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"
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
AuthorName string // something like 'Jesse Duffield'
|
||||
@@ -63,5 +82,9 @@ func (c *Commit) IsMerge() bool {
|
||||
// returns true if this commit is not actually in the git log but instead
|
||||
// is from a TODO file for an interactive rebase.
|
||||
func (c *Commit) IsTODO() bool {
|
||||
return c.Action != ""
|
||||
return c.Action != ActionNone
|
||||
}
|
||||
|
||||
func IsHeadCommit(commits []*Commit, index int) bool {
|
||||
return !commits[index].IsTODO() && (index == 0 || commits[index-1].IsTODO())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
@@ -28,6 +29,7 @@ type IFile interface {
|
||||
GetIsTracked() bool
|
||||
GetPath() string
|
||||
GetPreviousPath() string
|
||||
GetIsFile() bool
|
||||
}
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
@@ -43,7 +45,7 @@ func (f *File) Names() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
// returns true if the file names are the same or if a file rename includes the filename of the other
|
||||
func (f *File) Matches(f2 *File) bool {
|
||||
return utils.StringArraysOverlap(f.Names(), f2.Names())
|
||||
}
|
||||
@@ -90,3 +92,52 @@ func (f *File) GetPath() string {
|
||||
func (f *File) GetPreviousPath() string {
|
||||
return f.PreviousName
|
||||
}
|
||||
|
||||
func (f *File) GetIsFile() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type StatusFields struct {
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Deleted bool
|
||||
Added bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
ShortStatus string
|
||||
}
|
||||
|
||||
func SetStatusFields(file *File, shortStatus string) {
|
||||
derived := deriveStatusFields(shortStatus)
|
||||
|
||||
file.HasStagedChanges = derived.HasStagedChanges
|
||||
file.HasUnstagedChanges = derived.HasUnstagedChanges
|
||||
file.Tracked = derived.Tracked
|
||||
file.Deleted = derived.Deleted
|
||||
file.Added = derived.Added
|
||||
file.HasMergeConflicts = derived.HasMergeConflicts
|
||||
file.HasInlineMergeConflicts = derived.HasInlineMergeConflicts
|
||||
file.ShortStatus = derived.ShortStatus
|
||||
}
|
||||
|
||||
// shortStatus is something like '??' or 'A '
|
||||
func deriveStatusFields(shortStatus string) StatusFields {
|
||||
stagedChange := shortStatus[0:1]
|
||||
unstagedChange := shortStatus[1:2]
|
||||
tracked := !lo.Contains([]string{"??", "A ", "AM"}, shortStatus)
|
||||
hasStagedChanges := !lo.Contains([]string{" ", "U", "?"}, stagedChange)
|
||||
hasInlineMergeConflicts := lo.Contains([]string{"UU", "AA"}, shortStatus)
|
||||
hasMergeConflicts := hasInlineMergeConflicts || lo.Contains([]string{"DD", "AU", "UA", "UD", "DU"}, shortStatus)
|
||||
|
||||
return StatusFields{
|
||||
HasStagedChanges: hasStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: tracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || !tracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
ShortStatus: shortStatus,
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user