Compare commits
1257 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8619c07f3f | ||
|
|
7c538e3873 | ||
|
|
b1db21e10a | ||
|
|
1083006a55 | ||
|
|
693defde15 | ||
|
|
8b2de1171b | ||
|
|
0cf38b5def | ||
|
|
a57cbf2806 | ||
|
|
3ad42d23c4 | ||
|
|
e24fb45df1 | ||
|
|
6aeffb4be8 | ||
|
|
09b68f8f5b | ||
|
|
98e130f8c8 | ||
|
|
d47f29effb | ||
|
|
49555ce966 | ||
|
|
3cf7a818d1 | ||
|
|
b18becdec6 | ||
|
|
06a3f660b2 | ||
|
|
09d6726675 | ||
|
|
5d6731326e | ||
|
|
268d8ab175 | ||
|
|
28cd5e7bb2 | ||
|
|
e29b9010e5 | ||
|
|
485708539e | ||
|
|
84d39309db | ||
|
|
96cac13013 | ||
|
|
b65de976e6 | ||
|
|
13d75df6cd | ||
|
|
054c09a089 | ||
|
|
1c118d75f3 | ||
|
|
7747d829cc | ||
|
|
d3dda1ec92 | ||
|
|
71208ed570 | ||
|
|
450caed840 | ||
|
|
68ca5701f1 | ||
|
|
d68f366b1b | ||
|
|
7f9aa97ada | ||
|
|
463c411db5 | ||
|
|
18624da9ed | ||
|
|
050b6ed620 | ||
|
|
1099f11ff8 | ||
|
|
5527160424 | ||
|
|
4dae9f01d0 | ||
|
|
e21ebd0ac8 | ||
|
|
c06235a1d5 | ||
|
|
dd0c050b63 | ||
|
|
197bcb5fc9 | ||
|
|
6d62241c8b | ||
|
|
20fec1364f | ||
|
|
feed9ff133 | ||
|
|
b32db3c258 | ||
|
|
e53692e781 | ||
|
|
b6ce67dad3 | ||
|
|
9460a070a6 | ||
|
|
e1029b00d1 | ||
|
|
92e3b739d7 | ||
|
|
579a8278bb | ||
|
|
1e80480122 | ||
|
|
16fdfc27ea | ||
|
|
3dbf1202a0 | ||
|
|
3082396bf1 | ||
|
|
0cce6907a2 | ||
|
|
ffa6d9e57b | ||
|
|
a6d6f31885 | ||
|
|
3de0b23ef2 | ||
|
|
8a57155c4d | ||
|
|
37d3dd291a | ||
|
|
2ab4143d6b | ||
|
|
cb17cfe105 | ||
|
|
2374e00302 | ||
|
|
844a208506 | ||
|
|
b997820331 | ||
|
|
7265477da1 | ||
|
|
a3e6dade73 | ||
|
|
6320765f9a | ||
|
|
58490961c5 | ||
|
|
1032cccd55 | ||
|
|
35fa0ce8d2 | ||
|
|
598a1b5880 | ||
|
|
a1db2428d5 | ||
|
|
1a197a7d13 | ||
|
|
2ced3f9acc | ||
|
|
7c7698d5a2 | ||
|
|
eb48b3788f | ||
|
|
7674dae0cc | ||
|
|
3e98a40206 | ||
|
|
ec65b372e2 | ||
|
|
6bb216c0d4 | ||
|
|
88ace76db5 | ||
|
|
26a38183cf | ||
|
|
5f23781c10 | ||
|
|
dd475801ae | ||
|
|
db211e5ac7 | ||
|
|
e5ebb2239f | ||
|
|
180368621e | ||
|
|
1d2248571d | ||
|
|
f5893d991f | ||
|
|
047f1402de | ||
|
|
60912c0b47 | ||
|
|
8bff603a72 | ||
|
|
fd46b9a0c1 | ||
|
|
fdd5a19d20 | ||
|
|
57d7d00701 | ||
|
|
6685f9adb3 | ||
|
|
6d0a2ec115 | ||
|
|
950e703cab | ||
|
|
dea3d25bcc | ||
|
|
69fd7e664c | ||
|
|
07cd313087 | ||
|
|
2d7d1730ba | ||
|
|
7625277953 | ||
|
|
849824ab5a | ||
|
|
fe5e780719 | ||
|
|
fa3d751621 | ||
|
|
3b22a4f3b9 | ||
|
|
534da90b12 | ||
|
|
9fa80bf370 | ||
|
|
a2e86c2ea3 | ||
|
|
7065812c8d | ||
|
|
d9c199500f | ||
|
|
0a09c13a61 | ||
|
|
3453e4624d | ||
|
|
706cdf9243 | ||
|
|
ee5c243cbf | ||
|
|
ae7b7b0c21 | ||
|
|
1205cd620b | ||
|
|
9271b4116e | ||
|
|
9056e5e75c | ||
|
|
782ec070b4 | ||
|
|
baa1822180 | ||
|
|
e08d954573 | ||
|
|
0e3767102a | ||
|
|
7f88934d05 | ||
|
|
489918eb51 | ||
|
|
29c8f48bc8 | ||
|
|
711210b97b | ||
|
|
9781608584 | ||
|
|
7c41e99f98 | ||
|
|
44d4a8deb1 | ||
|
|
aa513b40e1 | ||
|
|
f67eed9c9d | ||
|
|
670b22eacf | ||
|
|
741de5fad5 | ||
|
|
7804ce7004 | ||
|
|
313220ae94 | ||
|
|
e299017e79 | ||
|
|
eeebb0a647 | ||
|
|
cc907996f4 | ||
|
|
7f75307f6e | ||
|
|
89f73d2ec2 | ||
|
|
156381155e | ||
|
|
369ea6a140 | ||
|
|
6a884d1388 | ||
|
|
65b57c4b60 | ||
|
|
dcd9a00ff3 | ||
|
|
ad32993721 | ||
|
|
92ef9f6fde | ||
|
|
3ede6b56f1 | ||
|
|
5755c096b4 | ||
|
|
979fa68837 | ||
|
|
a91132d187 | ||
|
|
597e944af9 | ||
|
|
4031b0d1a7 | ||
|
|
4ad2c4b271 | ||
|
|
4ccdeed3a2 | ||
|
|
1f3bd0ff70 | ||
|
|
35b2529c52 | ||
|
|
9b3a4eb3d8 | ||
|
|
6eafd9ead4 | ||
|
|
3c1e35eec7 | ||
|
|
381ab45d3a | ||
|
|
22aae374f1 | ||
|
|
0b0d057af4 | ||
|
|
5ad4cb0502 | ||
|
|
e2558fd33b | ||
|
|
02d8c6bca4 | ||
|
|
7756f0369a | ||
|
|
c95cea96af | ||
|
|
d7e9ffd4f9 | ||
|
|
7cf7a9e408 | ||
|
|
687d429c6f | ||
|
|
4afcd66754 | ||
|
|
9fd82f0d43 | ||
|
|
7e46e353ae | ||
|
|
6372f815e5 | ||
|
|
7f30be88e3 | ||
|
|
e611e79274 | ||
|
|
cb6441220c | ||
|
|
338dc2f4de | ||
|
|
cb4294a72b | ||
|
|
498be5fd1c | ||
|
|
8ff4ee3ba9 | ||
|
|
6ab6418974 | ||
|
|
491a95ba39 | ||
|
|
85d4399a17 | ||
|
|
a19c4c4cd2 | ||
|
|
d3fd54c690 | ||
|
|
e3e0189520 | ||
|
|
28c2677a75 | ||
|
|
50c6562492 | ||
|
|
232ad89a4d | ||
|
|
58b4e2c21e | ||
|
|
2701f48e8a | ||
|
|
4057e01df2 | ||
|
|
92cafb20ef | ||
|
|
8ea28a4e86 | ||
|
|
9b3bc7b2b9 | ||
|
|
99f6967144 | ||
|
|
36aed540e6 | ||
|
|
9847fe6e26 | ||
|
|
7ec62279e8 | ||
|
|
f505970778 | ||
|
|
e6a272b499 | ||
|
|
778ac16cf7 | ||
|
|
01ad0615b2 | ||
|
|
89db83839d | ||
|
|
40c79028ff | ||
|
|
d72617f5c2 | ||
|
|
69fdf90f39 | ||
|
|
149e8e4c62 | ||
|
|
fd0ccfb844 | ||
|
|
378f7fe446 | ||
|
|
d72de1d654 | ||
|
|
c2795a3819 | ||
|
|
c3bdc3ba8e | ||
|
|
66af52ae70 | ||
|
|
27e28d5f51 | ||
|
|
0ff55670b7 | ||
|
|
11bae8978d | ||
|
|
b50531156c | ||
|
|
181ddc5f3f | ||
|
|
22d3302b53 | ||
|
|
640bf333fc | ||
|
|
809cb41668 | ||
|
|
ea2f5f5ecc | ||
|
|
b49bacfac1 | ||
|
|
42f476baaf | ||
|
|
7c93ac9545 | ||
|
|
17118a9b9f | ||
|
|
6352cfe5bf | ||
|
|
4e40b56587 | ||
|
|
94924db632 | ||
|
|
8db448cab3 | ||
|
|
9c4bd29fc8 | ||
|
|
1943988f7e | ||
|
|
603116f491 | ||
|
|
a923896a62 | ||
|
|
a226611eae | ||
|
|
d85c6a7eac | ||
|
|
b4c0244a2a | ||
|
|
1346e5594c | ||
|
|
1866bf5f89 | ||
|
|
e7be093c39 | ||
|
|
fb1ba7c0b9 | ||
|
|
ba9adf40f4 | ||
|
|
d7765c4239 | ||
|
|
c4f517acb5 | ||
|
|
3643e80d5c | ||
|
|
15e444ee3a | ||
|
|
9c433b4de3 | ||
|
|
90a226927b | ||
|
|
f52586885e | ||
|
|
d98171b2aa | ||
|
|
2a590fd8cc | ||
|
|
569ed66fc2 | ||
|
|
efa8450bcc | ||
|
|
2c1fce4b48 | ||
|
|
6b588b4d45 | ||
|
|
11a0b9b0b7 | ||
|
|
7a51dbec4d | ||
|
|
4bb9fc8790 | ||
|
|
d4f9400e04 | ||
|
|
2cbeeeda95 | ||
|
|
dd1efee5cf | ||
|
|
02fa24e2f8 | ||
|
|
9dd3cf30e1 | ||
|
|
ab75103d00 | ||
|
|
b5d527bd97 | ||
|
|
3a86cb5ca7 | ||
|
|
7a37730076 | ||
|
|
f27b58114a | ||
|
|
314b1bd867 | ||
|
|
1580defa29 | ||
|
|
ea94d3a731 | ||
|
|
b958eaeddd | ||
|
|
2b8ef40289 | ||
|
|
0f5cf893f7 | ||
|
|
134ef734a4 | ||
|
|
0a5e2ef1ed | ||
|
|
7ad8265c54 | ||
|
|
4cb9500cd9 | ||
|
|
63a91fd281 | ||
|
|
6c9bb9f616 | ||
|
|
61e52d9bfa | ||
|
|
0289c82e4e | ||
|
|
4ef633a75a | ||
|
|
9ecc207270 | ||
|
|
b89672b134 | ||
|
|
ce923e6fe4 | ||
|
|
7fec560942 | ||
|
|
32fbea2870 | ||
|
|
8e00264943 | ||
|
|
0cb5b781a2 | ||
|
|
341c796ff8 | ||
|
|
ef45704a3c | ||
|
|
d7aa8c8924 | ||
|
|
bf4079a8a7 | ||
|
|
d3786992ba | ||
|
|
51ce196d45 | ||
|
|
017ca45aed | ||
|
|
d30de223fe | ||
|
|
95bbbda9af | ||
|
|
72b954a1a5 | ||
|
|
6ec7eea02b | ||
|
|
3229c5d5ed | ||
|
|
423b5ff3d1 | ||
|
|
5d06aac1ad | ||
|
|
9995a28e54 | ||
|
|
038c4a50d1 | ||
|
|
65410e7293 | ||
|
|
f126f7a9f6 | ||
|
|
cb1e8da619 | ||
|
|
7f89962f72 | ||
|
|
95e1d4d432 | ||
|
|
e818a6a35e | ||
|
|
7f161d61cd | ||
|
|
671c8416e2 | ||
|
|
23d6cbe0f2 | ||
|
|
43fac4f12a | ||
|
|
09747c2248 | ||
|
|
a89044fd94 | ||
|
|
baac3d01be | ||
|
|
d1ebc56bfe | ||
|
|
a085add733 | ||
|
|
ff59eb6c10 | ||
|
|
659ee74b04 | ||
|
|
d3186b5d10 | ||
|
|
b925155b7f | ||
|
|
6ad80ea3a9 | ||
|
|
dd255837ab | ||
|
|
53e9c8d1ef | ||
|
|
93fee307d6 | ||
|
|
9be28c9af9 | ||
|
|
f56844501e | ||
|
|
3dca5feeec | ||
|
|
fa2515343b | ||
|
|
a92be3be9d | ||
|
|
7651fdba89 | ||
|
|
58f5326fb8 | ||
|
|
98e0379736 | ||
|
|
4fe52dc117 | ||
|
|
b5b2eb6007 | ||
|
|
1a582dacc4 | ||
|
|
be7daa60c5 | ||
|
|
6e12c1e974 | ||
|
|
c57543b4f8 | ||
|
|
5ad54ee675 | ||
|
|
c2d23da543 | ||
|
|
a61677b9e6 | ||
|
|
64a1e3866e | ||
|
|
920c28cfd7 | ||
|
|
d6ab75399b | ||
|
|
4fc55a2263 | ||
|
|
7b437d030f | ||
|
|
c55188e462 | ||
|
|
3de9953dde | ||
|
|
218d739b03 | ||
|
|
4bf1290610 | ||
|
|
a72c62bd82 | ||
|
|
56e2da3027 | ||
|
|
f14bf271c7 | ||
|
|
f27ca92f10 | ||
|
|
09a2f70c89 | ||
|
|
b08eddb45b | ||
|
|
fe05f93a74 | ||
|
|
665e99779e | ||
|
|
945ec45027 | ||
|
|
5d68ea0e39 | ||
|
|
5053850433 | ||
|
|
f91621be09 | ||
|
|
21491bcbca | ||
|
|
188d00159b | ||
|
|
29fb29638f | ||
|
|
48b6c07843 | ||
|
|
44e1c56a1c | ||
|
|
2ec6d632ed | ||
|
|
0966953050 | ||
|
|
3b7b443e2a | ||
|
|
5fafe784d1 | ||
|
|
4cc6b459cc | ||
|
|
89e9f855cb | ||
|
|
92e0face1e | ||
|
|
8272b20913 | ||
|
|
ba26a1a664 | ||
|
|
bb2640e893 | ||
|
|
9d94b9f600 | ||
|
|
ef6abbba0f | ||
|
|
3342544dfb | ||
|
|
6515831c95 | ||
|
|
93959aff5d | ||
|
|
5b3e2c9ae3 | ||
|
|
5131aba138 | ||
|
|
540a95c160 | ||
|
|
9c8c802b20 | ||
|
|
76814a36e3 | ||
|
|
7d8b7c9239 | ||
|
|
b15c8643c8 | ||
|
|
3d06b54d2c | ||
|
|
f9ebba3043 | ||
|
|
205555916c | ||
|
|
3db4425b98 | ||
|
|
a4e08834e2 | ||
|
|
512f2f439e | ||
|
|
bb92067b19 | ||
|
|
dddb35ac15 | ||
|
|
f40d368069 | ||
|
|
b7b7705df2 | ||
|
|
5dd1852dd3 | ||
|
|
02dfae7bd4 | ||
|
|
96116ce405 | ||
|
|
1a1c948020 | ||
|
|
b43c4c05f6 | ||
|
|
99b9a38fb3 | ||
|
|
baddec0e39 | ||
|
|
0fba25506a | ||
|
|
0ab1ff8625 | ||
|
|
2274d7c22f | ||
|
|
43e44bf02a | ||
|
|
21810a4df6 | ||
|
|
43af49bab4 | ||
|
|
f130cd5068 | ||
|
|
ba5d89595e | ||
|
|
f68aba0ffa | ||
|
|
7556282f17 | ||
|
|
7cda59d87e | ||
|
|
3b7e1f921a | ||
|
|
3397ab5c75 | ||
|
|
e3b036daa5 | ||
|
|
04d3587be2 | ||
|
|
7de1adc476 | ||
|
|
aa54d6f8a6 | ||
|
|
11401872b5 | ||
|
|
e65d25da60 | ||
|
|
8008e04c6b | ||
|
|
b39a24db06 | ||
|
|
f4d4a5e086 | ||
|
|
fd79b2d5e3 | ||
|
|
598cc778d4 | ||
|
|
d6818ecc1a | ||
|
|
c629070ea5 | ||
|
|
5cff3221a1 | ||
|
|
456d50919c | ||
|
|
4da1cc4473 | ||
|
|
8876c1d0f6 | ||
|
|
2a2f914839 | ||
|
|
7bf65f4835 | ||
|
|
3f69f5fe62 | ||
|
|
9f0ef851b2 | ||
|
|
fa051eff5a | ||
|
|
a50a478c1a | ||
|
|
2bbaa6f3a1 | ||
|
|
5dad9f4adb | ||
|
|
762b889b48 | ||
|
|
2b39c6465a | ||
|
|
45c34e5675 | ||
|
|
1a6f82b71b | ||
|
|
720e5debb8 | ||
|
|
8fb6f23ff3 | ||
|
|
e2590f79bd | ||
|
|
286283d3ed | ||
|
|
90a109b1dd | ||
|
|
8b665a43fc | ||
|
|
f0f90a88f1 | ||
|
|
1cb821cd51 | ||
|
|
cae864a188 | ||
|
|
e22cc70a7d | ||
|
|
af9da95178 | ||
|
|
ab95b98ef8 | ||
|
|
be10d9096a | ||
|
|
acf4661c1e | ||
|
|
e7bc4084da | ||
|
|
f1da79b4b2 | ||
|
|
f4a7034a9a | ||
|
|
e1f243d8fb | ||
|
|
825935ba4f | ||
|
|
b9a2e131f2 | ||
|
|
3439862854 | ||
|
|
89ce540653 | ||
|
|
84d855d242 | ||
|
|
1b8f9772f4 | ||
|
|
7335cd1c5d | ||
|
|
1fa6f9b725 | ||
|
|
4c33ee4859 | ||
|
|
61ee3fd695 | ||
|
|
0383f9517b | ||
|
|
5259fd90b3 | ||
|
|
8bdba9ce7c | ||
|
|
673edd8f2d | ||
|
|
d5f88df88a | ||
|
|
0a3736e765 | ||
|
|
961b731c59 | ||
|
|
c30eb7c0a8 | ||
|
|
4f89ad5be4 | ||
|
|
1487fe3433 | ||
|
|
b0922080e3 | ||
|
|
9b00036937 | ||
|
|
21ce2015a0 | ||
|
|
4fec18fa4e | ||
|
|
f21c05bd00 | ||
|
|
f6497140fa | ||
|
|
e34a4f82ea | ||
|
|
0beff5c569 | ||
|
|
975e881d83 | ||
|
|
307d38ccc9 | ||
|
|
32d0a5c516 | ||
|
|
d9ccd9e5d3 | ||
|
|
8940c472ca | ||
|
|
99c705eaa7 | ||
|
|
55d316fad9 | ||
|
|
e7c61ffc89 | ||
|
|
7b7c5c4131 | ||
|
|
521ab91309 | ||
|
|
fd400cfcc7 | ||
|
|
a6416b914d | ||
|
|
d4dd58f6ca | ||
|
|
fabed3238b | ||
|
|
53834f2415 | ||
|
|
83df6ddbc8 | ||
|
|
91242e574a | ||
|
|
c39eb93736 | ||
|
|
408e2caca4 | ||
|
|
bcf9bac934 | ||
|
|
d576405223 | ||
|
|
77d1660066 | ||
|
|
4a0930ae02 | ||
|
|
25cdc198d2 | ||
|
|
9850d02f7d | ||
|
|
6d91a43004 | ||
|
|
e20cf918ed | ||
|
|
985acf23ef | ||
|
|
b051b3c00c | ||
|
|
187fbbdcf5 | ||
|
|
501833cd90 | ||
|
|
91ad02c105 | ||
|
|
d285440f2b | ||
|
|
4abe5edf06 | ||
|
|
2be0e73d5b | ||
|
|
0e2b3db1d9 | ||
|
|
2fd957e2c8 | ||
|
|
09907f3873 | ||
|
|
b7772e63e4 | ||
|
|
579f5e1e37 | ||
|
|
81a69f845e | ||
|
|
9bdd5cc856 | ||
|
|
0bd5b9958e | ||
|
|
52cb83f714 | ||
|
|
ca15fcbe4c | ||
|
|
b2e7420349 | ||
|
|
9bd483fea5 | ||
|
|
bd96c85f5a | ||
|
|
4eea01a9fe | ||
|
|
8775fec9fa | ||
|
|
aa7aa7a5c1 | ||
|
|
d0f15d54ab | ||
|
|
8768611876 | ||
|
|
015d409540 | ||
|
|
5b81d72014 | ||
|
|
6bda5c1f21 | ||
|
|
7c0d010233 | ||
|
|
bf2c1e3295 | ||
|
|
253a18f62a | ||
|
|
0e1d83fb02 | ||
|
|
5808515853 | ||
|
|
ecb793206d | ||
|
|
19051cfec0 | ||
|
|
0a8cd66bbd | ||
|
|
11b9e8193c | ||
|
|
6356f64b1d | ||
|
|
42043bda6f | ||
|
|
52dfefe624 | ||
|
|
ebe41e8a75 | ||
|
|
5be397b335 | ||
|
|
16c97edb4d | ||
|
|
082e308efb | ||
|
|
bc1cea3dc8 | ||
|
|
4fd056c38b | ||
|
|
85ab9d47ad | ||
|
|
f2f51cde82 | ||
|
|
bd139ccc59 | ||
|
|
e0bc51822f | ||
|
|
ea9314e242 | ||
|
|
0c591dfea4 | ||
|
|
c38b1d1e1c | ||
|
|
f639f4a0d0 | ||
|
|
b36ffc9481 | ||
|
|
0376ed3a26 | ||
|
|
8c177151d1 | ||
|
|
15e9222f51 | ||
|
|
86c4f7ff1c | ||
|
|
7eaa04169f | ||
|
|
6ce3485cf0 | ||
|
|
33ac5b97ae | ||
|
|
9a7c2199a7 | ||
|
|
2675934027 | ||
|
|
c68fa3e87b | ||
|
|
005047f015 | ||
|
|
0fdec134c5 | ||
|
|
5b2b8c7e0a | ||
|
|
6339a1f33c | ||
|
|
c84a973d5d | ||
|
|
11c65f633f | ||
|
|
2377924f9c | ||
|
|
7558d25e25 | ||
|
|
a5b898c718 | ||
|
|
9e69f251a8 | ||
|
|
30211b2a6b | ||
|
|
306ff91afa | ||
|
|
ab51490648 | ||
|
|
b4a77c9262 | ||
|
|
77bb69a162 | ||
|
|
0cf041bb98 | ||
|
|
cde1c7f883 | ||
|
|
d253022f13 | ||
|
|
4a6131fcd9 | ||
|
|
3c5131ad27 | ||
|
|
b50d44a4b8 | ||
|
|
514e8f0175 | ||
|
|
074820e63b | ||
|
|
ecf6fe9369 | ||
|
|
77d1ef6874 | ||
|
|
809281f1ab | ||
|
|
347e6e43e7 | ||
|
|
8437b76f99 | ||
|
|
798f990cd3 | ||
|
|
bf31f20657 | ||
|
|
6ec647710d | ||
|
|
9eb8d470a4 | ||
|
|
e90656d61c | ||
|
|
7400d5bc68 | ||
|
|
53988ba4e0 | ||
|
|
495d4d5da7 | ||
|
|
403c5aabd9 | ||
|
|
a828bd2cea | ||
|
|
5aeb9c4c9c | ||
|
|
38bf345ebf | ||
|
|
9e9e743515 | ||
|
|
965f0765aa | ||
|
|
104078f15f | ||
|
|
9d37e36314 | ||
|
|
a87f66c131 | ||
|
|
104e5bf62e | ||
|
|
e661ee6e3e | ||
|
|
dba5206e46 | ||
|
|
77f672570f | ||
|
|
b935df59a1 | ||
|
|
46a039f88f | ||
|
|
023f09b327 | ||
|
|
c1e209581b | ||
|
|
bfa9f7ef01 | ||
|
|
4682a1b75b | ||
|
|
bdba065909 | ||
|
|
cb9cf3ad0c | ||
|
|
05ce018e06 | ||
|
|
c2435c2752 | ||
|
|
b4450f9bc3 | ||
|
|
5c98e2fe76 | ||
|
|
c119e6ae18 | ||
|
|
3c9c266c01 | ||
|
|
999912c347 | ||
|
|
7ebb3c0ef6 | ||
|
|
022b389146 | ||
|
|
e90e8dc536 | ||
|
|
be801a336f | ||
|
|
bfcf33fce4 | ||
|
|
197fc6fdf1 | ||
|
|
40cd72abfc | ||
|
|
0ca82aba46 | ||
|
|
58e72cd22b | ||
|
|
4f3be697d5 | ||
|
|
195a6f2c90 | ||
|
|
ff48840f23 | ||
|
|
4f10dfb185 | ||
|
|
7d5b7d2c38 | ||
|
|
022281d414 | ||
|
|
bc99a92ec8 | ||
|
|
1a96855c63 | ||
|
|
036cc16417 | ||
|
|
2b3bc622d6 | ||
|
|
cdaf3b8b84 | ||
|
|
aa4266cd1f | ||
|
|
d6f33532bb | ||
|
|
8fa96fd598 | ||
|
|
49cd7ea2db | ||
|
|
9b09617f85 | ||
|
|
370aff5fcc | ||
|
|
836e03c01c | ||
|
|
48d42abd38 | ||
|
|
379bf265fe | ||
|
|
3a6f292bf5 | ||
|
|
a921ab543b | ||
|
|
904885e001 | ||
|
|
19b820bb35 | ||
|
|
8d6e30d2a6 | ||
|
|
3e0ec29d0d | ||
|
|
2554f04ace | ||
|
|
3af256c75a | ||
|
|
b15d24caf8 | ||
|
|
b2d123b519 | ||
|
|
060ea8f0c6 | ||
|
|
2611f5a13e | ||
|
|
12fdfeafa8 | ||
|
|
c1e3e978a2 | ||
|
|
76258fbb93 | ||
|
|
5ac6144c35 | ||
|
|
75abd0f283 | ||
|
|
9782eb7b1f | ||
|
|
00182d1c5e | ||
|
|
cdae2b067a | ||
|
|
0052cab3df | ||
|
|
1a298a4e72 | ||
|
|
6abfacb5fd | ||
|
|
63f230f0d1 | ||
|
|
3ceeb33c25 | ||
|
|
73db2cfd03 | ||
|
|
88a3efbe06 | ||
|
|
6c38c65716 | ||
|
|
45bb8a71b5 | ||
|
|
f8e1c26309 | ||
|
|
5df48021d8 | ||
|
|
eba650a4c8 | ||
|
|
017823da49 | ||
|
|
01f6580125 | ||
|
|
8f29cb2e81 | ||
|
|
670677044f | ||
|
|
2fa4c7932c | ||
|
|
e1a40dd224 | ||
|
|
35e1a0b735 | ||
|
|
258299265a | ||
|
|
c3e318fdd5 | ||
|
|
000deb2cf8 | ||
|
|
fa551878a8 | ||
|
|
10bf4e310d | ||
|
|
9d83ce358e | ||
|
|
ab01fc7e35 | ||
|
|
9d256510ac | ||
|
|
ba9b6d6b6a | ||
|
|
8f612c5cb4 | ||
|
|
0fb1856d18 | ||
|
|
51d7616828 | ||
|
|
d4d6fd28eb | ||
|
|
aefc18d819 | ||
|
|
0fca8befc8 | ||
|
|
8f7f35b8a9 | ||
|
|
201561bc7a | ||
|
|
f0b491d0ec | ||
|
|
06289afe6e | ||
|
|
7f5c6ceeef | ||
|
|
bff1c33625 | ||
|
|
8ab62244ce | ||
|
|
1a0167e7f8 | ||
|
|
57a5322fa7 | ||
|
|
5411397f9a | ||
|
|
0e0cdb327b | ||
|
|
54fdf3d51c | ||
|
|
32f6134309 | ||
|
|
b424b9beeb | ||
|
|
ce70d5ef12 | ||
|
|
48eed0562a | ||
|
|
c663e6b0a9 | ||
|
|
582b660bf7 | ||
|
|
1a833fcec3 | ||
|
|
8088aa9743 | ||
|
|
a5128b94dd | ||
|
|
ad45102e3b | ||
|
|
1dc097ebd8 | ||
|
|
b8a436fdeb | ||
|
|
ad42ad9269 | ||
|
|
5ca712ff80 | ||
|
|
c7356ae3de | ||
|
|
9fa5fddd93 | ||
|
|
f29178d1b3 | ||
|
|
3ff5b51fe9 | ||
|
|
00040a39cb | ||
|
|
71ba561221 | ||
|
|
0b1a0a148c | ||
|
|
c6dec379f2 | ||
|
|
e5c62d01e1 | ||
|
|
761cb10405 | ||
|
|
fce6c51bea | ||
|
|
01188e214e | ||
|
|
ece5a86855 | ||
|
|
669ea61019 | ||
|
|
03dd0de07a | ||
|
|
81645a61d4 | ||
|
|
438fa8ec19 | ||
|
|
9ca6068a17 | ||
|
|
b59526c86c | ||
|
|
55a006a4a0 | ||
|
|
15500246ca | ||
|
|
289eddd9be | ||
|
|
7dcf93e0b2 | ||
|
|
e8b7097845 | ||
|
|
fa302717e8 | ||
|
|
045e9e5f09 | ||
|
|
b9b01cd32a | ||
|
|
c1305f47b2 | ||
|
|
71ea7fb89d | ||
|
|
9210d8ca87 | ||
|
|
e902f4ce3e | ||
|
|
c24db1189e | ||
|
|
6c4ee56eed | ||
|
|
ca19d42948 | ||
|
|
9b46bb63f9 | ||
|
|
8da9cfc21d | ||
|
|
8d2de65868 | ||
|
|
b987598c7b | ||
|
|
234e7cb3fc | ||
|
|
0b0f550572 | ||
|
|
83d6941c7d | ||
|
|
4ef9659138 | ||
|
|
bea70306e2 | ||
|
|
8e8c5fad55 | ||
|
|
bc15b5d550 | ||
|
|
fbcf908881 | ||
|
|
289f4f3630 | ||
|
|
bbcadcb5d1 | ||
|
|
8cdb02349f | ||
|
|
92f63d107c | ||
|
|
3fee481e8d | ||
|
|
3667db37e1 | ||
|
|
faf912393b | ||
|
|
b495425b16 | ||
|
|
5495e9bec3 | ||
|
|
425e9f091c | ||
|
|
7efa225f31 | ||
|
|
5c191b9cf3 | ||
|
|
b693d33c32 | ||
|
|
9cfad7b1d0 | ||
|
|
c6e7a55fbf | ||
|
|
c67c2cf622 | ||
|
|
c03a220990 | ||
|
|
6b9a91f17d | ||
|
|
b6ed33037e | ||
|
|
fbab49b858 | ||
|
|
0f9389c284 | ||
|
|
45d850e8ca | ||
|
|
5430a4d21d | ||
|
|
5876d617ff | ||
|
|
dbe5a37372 | ||
|
|
2579d6bd6a | ||
|
|
e371153034 | ||
|
|
a172b18428 | ||
|
|
a4943224bd | ||
|
|
282e578ac3 | ||
|
|
c705712a05 | ||
|
|
e5acee1a0c | ||
|
|
fa2ad81bec | ||
|
|
98f6bd4e77 | ||
|
|
d92d43afef | ||
|
|
5b69f7713f | ||
|
|
9c2d8c0e0d | ||
|
|
c7e54fa17d | ||
|
|
ac67e53b12 | ||
|
|
6dfe5ea62a | ||
|
|
6b5745f6c2 | ||
|
|
e2a0f3800f | ||
|
|
5b7686bf92 | ||
|
|
f04af21382 | ||
|
|
216fad3140 | ||
|
|
8604b331ae | ||
|
|
eeb502d0ff | ||
|
|
049272ea2b | ||
|
|
11adcce84e | ||
|
|
5cc4c63066 | ||
|
|
df43118e55 | ||
|
|
aeb64e1175 | ||
|
|
ff0c123293 | ||
|
|
16a537c102 | ||
|
|
e0fa63c6c9 | ||
|
|
9534e4c2f9 | ||
|
|
cb01fda516 | ||
|
|
f69460cccf | ||
|
|
f2b2665c78 | ||
|
|
5906a2789d | ||
|
|
882b890738 | ||
|
|
d1566861dd | ||
|
|
ddf8c69253 | ||
|
|
0a970db24a | ||
|
|
30918d114c | ||
|
|
e38118836e | ||
|
|
eeaed52cd5 | ||
|
|
c99cdab614 | ||
|
|
90c5d11e56 | ||
|
|
89b499274e | ||
|
|
7ad7f4cbc4 | ||
|
|
67fa456e08 | ||
|
|
1020346217 | ||
|
|
69d98b596e | ||
|
|
1d9c1403b9 | ||
|
|
0ddec47611 | ||
|
|
589536c2c3 | ||
|
|
609039eb5e | ||
|
|
cf9ce9d4ff | ||
|
|
5f7213730b | ||
|
|
5101b39556 | ||
|
|
4249a278b6 | ||
|
|
f308bddb77 | ||
|
|
862c6b160a | ||
|
|
cbb51bd535 | ||
|
|
77a20fb86f | ||
|
|
b9c105605e | ||
|
|
4e0da37230 | ||
|
|
986d34a5ac | ||
|
|
aa9ed3349f | ||
|
|
79e42391ac | ||
|
|
a79967fcd8 | ||
|
|
65b121179d | ||
|
|
69d30a2cd0 | ||
|
|
bacf81f6d6 | ||
|
|
f4f560ce5f | ||
|
|
8c32ec53ee | ||
|
|
0879c2ff9e | ||
|
|
3c2df73371 | ||
|
|
1787917c13 | ||
|
|
05d456a462 | ||
|
|
7068332095 | ||
|
|
ef3ece552d | ||
|
|
bcb565788e | ||
|
|
d6a84c1bd2 | ||
|
|
e68e5e2f46 | ||
|
|
586acc434d | ||
|
|
200563aa58 | ||
|
|
12a6ecb9f7 | ||
|
|
b71052ef9d | ||
|
|
b9dba008aa | ||
|
|
79f1be0983 | ||
|
|
0e2262882b | ||
|
|
8e54bfd364 | ||
|
|
2eee7e9b0e | ||
|
|
d040b33559 | ||
|
|
02f1555103 | ||
|
|
6630dca197 | ||
|
|
d4949a676b | ||
|
|
074bb7cdb5 | ||
|
|
dbc927779a | ||
|
|
8986f70a89 | ||
|
|
caa7840091 | ||
|
|
948f4bb287 | ||
|
|
7d9e6f8c4d | ||
|
|
64f68fa668 | ||
|
|
594dd12864 | ||
|
|
fa8d8aff62 | ||
|
|
4a0e58ae15 | ||
|
|
70e6a3db6e | ||
|
|
3ce715178b | ||
|
|
32a9dbf43e | ||
|
|
8fd5d991bd | ||
|
|
6b424403b3 | ||
|
|
0d9b6e3fef | ||
|
|
300e799aea | ||
|
|
edf798b91b | ||
|
|
1985fd2dbc | ||
|
|
c1cb868c4b | ||
|
|
ad0ec7e9b8 | ||
|
|
0ab88f9cb2 | ||
|
|
149841c604 | ||
|
|
cce49a34b7 | ||
|
|
c3dbce1cd6 | ||
|
|
638d7c2648 | ||
|
|
5f466ff983 | ||
|
|
13afbf6bba | ||
|
|
da531c61f6 | ||
|
|
02efae1499 | ||
|
|
96aa346292 | ||
|
|
ece358fcc0 | ||
|
|
bdf90e94da | ||
|
|
3c27d4f9f5 | ||
|
|
b18cabf4d3 | ||
|
|
8dafa1266e | ||
|
|
505e01664f | ||
|
|
c807ae4950 | ||
|
|
fefe9450bd | ||
|
|
3743983b70 | ||
|
|
2a578889f3 | ||
|
|
dadf8ba269 | ||
|
|
c2d57fca98 | ||
|
|
a620d1a01c | ||
|
|
f1b2a43f53 | ||
|
|
df7ed1a6d0 | ||
|
|
b0eaca72cb | ||
|
|
3838fe74df | ||
|
|
3347003fff | ||
|
|
970931abfc | ||
|
|
02170ee280 | ||
|
|
fa541fb797 | ||
|
|
d7f61ba321 | ||
|
|
77472d086f | ||
|
|
9db318ff1f | ||
|
|
86798008ed | ||
|
|
284c57fb72 | ||
|
|
e9d8de1be4 | ||
|
|
4f3ecfcd7c | ||
|
|
750b45a6c4 | ||
|
|
2745d9f860 | ||
|
|
6e0e4b57b5 | ||
|
|
9ace5357e1 | ||
|
|
457ea47457 | ||
|
|
1013d3fa24 | ||
|
|
194fa6f2e7 | ||
|
|
ef78a3a8ba | ||
|
|
f2b09d39c6 | ||
|
|
b622ceef94 | ||
|
|
3e72539ca1 | ||
|
|
e18aa48aea | ||
|
|
11c055220d | ||
|
|
ec8effd963 | ||
|
|
fce9b90071 | ||
|
|
53e855e488 | ||
|
|
bfba218fb4 | ||
|
|
6cf39a88f5 | ||
|
|
acb7e52696 | ||
|
|
160bc6b99d | ||
|
|
98625272fb | ||
|
|
d08fb976e0 | ||
|
|
d51f9c5d4b | ||
|
|
390c65bf37 | ||
|
|
317b245ba0 | ||
|
|
9653545c81 | ||
|
|
614040b854 | ||
|
|
14c79c12a6 | ||
|
|
8db28d485a | ||
|
|
36e2c4a86e | ||
|
|
47bc83f243 | ||
|
|
8b00d6756c | ||
|
|
12fbfa0946 | ||
|
|
0874f33eb3 | ||
|
|
166826f5ea | ||
|
|
a1f3931b59 | ||
|
|
435de9cda3 | ||
|
|
449dc43a5f | ||
|
|
212160a4cb | ||
|
|
b51734cce8 | ||
|
|
13b6b2fdc6 | ||
|
|
060380fdd6 | ||
|
|
a3d4f2bd5f | ||
|
|
9118ed1f30 | ||
|
|
3fdf5ba618 | ||
|
|
132559ea7f | ||
|
|
d6ace56288 | ||
|
|
bc0895773c | ||
|
|
25cffab65b | ||
|
|
a6ad95a04a | ||
|
|
ec6e5987ad | ||
|
|
b6429bc330 | ||
|
|
1fd81b463e | ||
|
|
6b83956138 | ||
|
|
6f70652d42 | ||
|
|
d5d36de01e | ||
|
|
508ee35ce5 | ||
|
|
566c449384 | ||
|
|
e7f15ae457 | ||
|
|
4b03e92cb9 | ||
|
|
2c5bd289bf | ||
|
|
8dc669893f | ||
|
|
d790b48f5d | ||
|
|
10b4704662 | ||
|
|
cef0de4f0e | ||
|
|
55b9877e93 | ||
|
|
12f3d97ef0 | ||
|
|
70ce3f88d6 | ||
|
|
661e429082 | ||
|
|
5550415e3c | ||
|
|
9eb30ecb4c | ||
|
|
7c11800250 | ||
|
|
6249491484 | ||
|
|
cd639b29c0 | ||
|
|
eddc703fb8 | ||
|
|
a947ce35e7 | ||
|
|
0a6ca35fc7 | ||
|
|
cb1d801be0 | ||
|
|
006cdd6373 | ||
|
|
ecabee02da | ||
|
|
36699a0a36 | ||
|
|
328061dec6 | ||
|
|
5a7c85daee | ||
|
|
8fb506520c | ||
|
|
f4ee24055b | ||
|
|
5c94c18c7b | ||
|
|
74c3d6f21c | ||
|
|
26318c10e9 | ||
|
|
304c0a3a6b | ||
|
|
a847ea2a92 | ||
|
|
444e14631d | ||
|
|
9038b1d07d | ||
|
|
3bc4feb2ac | ||
|
|
4d05dea4a6 | ||
|
|
33ac72c8e7 | ||
|
|
ee7ec691f0 | ||
|
|
d285ec0b09 | ||
|
|
cbf53d6013 | ||
|
|
8466b39226 | ||
|
|
21a10a618a | ||
|
|
ab908f83df | ||
|
|
d617ba76fa | ||
|
|
f689c428b2 | ||
|
|
38cf196b62 | ||
|
|
1d409d31ab | ||
|
|
cd3989238b | ||
|
|
95b2737d9b | ||
|
|
3db1a68515 | ||
|
|
e548e8c75f | ||
|
|
dbf7661521 | ||
|
|
11d1f43830 | ||
|
|
a55dcf62a3 | ||
|
|
3c8eb7e049 | ||
|
|
fa7cd37ca7 | ||
|
|
0360bdf080 | ||
|
|
c42c9f900a | ||
|
|
ae15bb41d3 | ||
|
|
d7c5bc583f | ||
|
|
056ffb166d | ||
|
|
18e61efae6 | ||
|
|
389bd75d46 | ||
|
|
5c661be159 | ||
|
|
dee97a77c4 | ||
|
|
271d3f696c | ||
|
|
899168e1ce | ||
|
|
153c79a828 | ||
|
|
31f677187b | ||
|
|
d6a2af89ee | ||
|
|
035870e52f | ||
|
|
d1dcdab288 | ||
|
|
cd3a1e2793 | ||
|
|
39400aac92 | ||
|
|
d78b029c21 | ||
|
|
6ae5b5e2b2 | ||
|
|
8486233053 | ||
|
|
d84120a895 | ||
|
|
c6abbaf4d4 | ||
|
|
8596e01dcd | ||
|
|
d359fabe7b | ||
|
|
48f9331b0d | ||
|
|
5f34e9cc07 | ||
|
|
e4c7867564 | ||
|
|
9f8fc6b907 | ||
|
|
23516f1a41 | ||
|
|
f8bad7d541 | ||
|
|
07f5d69a48 | ||
|
|
b73b20072f | ||
|
|
8030ac8867 | ||
|
|
d2bdec43a1 | ||
|
|
05dd8eff1d | ||
|
|
915cece734 | ||
|
|
d541d0d9f8 | ||
|
|
5d01da1c93 | ||
|
|
1bc137ee35 | ||
|
|
da99aa07bd | ||
|
|
4993cc8f35 | ||
|
|
99bdb46795 | ||
|
|
fa051e9226 | ||
|
|
8729530ba9 | ||
|
|
33344e93e2 | ||
|
|
fc4ed855d6 | ||
|
|
7dd6b4e8d8 | ||
|
|
1a8caa32d4 | ||
|
|
23e52b7c87 | ||
|
|
22037f8991 | ||
|
|
495b0acbb7 | ||
|
|
417db6ed6c | ||
|
|
a12180e2e2 | ||
|
|
28a390cfdd | ||
|
|
62229f4b81 | ||
|
|
f27227af41 | ||
|
|
bf56d3bff2 | ||
|
|
cfea72d709 | ||
|
|
b70ea92608 | ||
|
|
1e7258b617 | ||
|
|
6be4da873b | ||
|
|
6d6f60349a | ||
|
|
26a9aaacf5 | ||
|
|
24217a12c3 | ||
|
|
fb2b990072 | ||
|
|
3b5d43ecb2 | ||
|
|
aaf0e4cfbf | ||
|
|
6f2157c1d8 | ||
|
|
40e03ba7de | ||
|
|
0454e2a1cd | ||
|
|
b9e4631ff4 | ||
|
|
e5688e3b8b | ||
|
|
2a2751bf2a | ||
|
|
8905fd22ea | ||
|
|
777d362dfc | ||
|
|
3a1c1a6b99 | ||
|
|
361e1d86e9 | ||
|
|
26a3b65312 | ||
|
|
63ab78ffb7 | ||
|
|
084be75aa3 | ||
|
|
e275caa872 | ||
|
|
9c7ac0f84d | ||
|
|
8353dfdd36 | ||
|
|
a8654329ec | ||
|
|
f51a3a953a | ||
|
|
6524af65a1 | ||
|
|
e500302b8b | ||
|
|
d3a6fdd2e7 | ||
|
|
fa1a1c8727 | ||
|
|
6b9e211f34 | ||
|
|
60d512a5b3 | ||
|
|
7e9c988f50 | ||
|
|
70e1e31076 | ||
|
|
49a1855cbb | ||
|
|
eef1a79375 | ||
|
|
81924fb9f8 | ||
|
|
3db3b95dd0 | ||
|
|
5672cfd033 | ||
|
|
e35b196db6 | ||
|
|
a5c026378d | ||
|
|
dafbb4c4f3 | ||
|
|
68efbe20d7 | ||
|
|
f23e8df69a | ||
|
|
13a0f4e9e2 | ||
|
|
1faba1760c | ||
|
|
bc611bca52 | ||
|
|
6abc7d297f | ||
|
|
f7ecc0e194 | ||
|
|
2402777427 | ||
|
|
7cc19f673a | ||
|
|
923323bd71 | ||
|
|
ee9a8a6a9f | ||
|
|
71e5cf1726 | ||
|
|
2b85b81a3e | ||
|
|
5c694bd696 | ||
|
|
3847ec92cf | ||
|
|
dad8e8d43d | ||
|
|
25a49e22f2 | ||
|
|
a4bdfd16c1 | ||
|
|
0b48e6f4ff | ||
|
|
55f224c5ca | ||
|
|
274dedb5e8 | ||
|
|
8493ab47e6 | ||
|
|
d4db614a01 | ||
|
|
56502ad3fd | ||
|
|
62ea1dea04 | ||
|
|
47ff93734e | ||
|
|
047939ba6e | ||
|
|
d42b00389c | ||
|
|
e16dfcaee7 | ||
|
|
71b398e858 | ||
|
|
71e3d9a9b8 | ||
|
|
29f71f50d4 | ||
|
|
d6f43f063e | ||
|
|
0f896933a5 | ||
|
|
c0022d4eea | ||
|
|
36e9f7754b | ||
|
|
8f12010aa8 | ||
|
|
c9a6978fe0 | ||
|
|
19f2fd03cf |
8
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.arm-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
msrv = "1.50.0"
|
||||
cognitive-complexity-threshold = 18
|
||||
msrv = "1.88.0"
|
||||
cognitive-complexity-threshold = 18
|
||||
|
|
|
|||
3
.editorconfig
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
root = true
|
||||
[*.rs]
|
||||
indent_style = tab
|
||||
2
.github/FUNDING.yml
vendored
|
|
@ -1 +1 @@
|
|||
github: extrawurst
|
||||
github: extrawurst
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -2,7 +2,7 @@
|
|||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -2,7 +2,7 @@
|
|||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'feature-request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
|
|
|||
16
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!---
|
||||
Thank you for contributing to GitUI! Please fill out the template below, and remove or add any
|
||||
information as you feel necessary.
|
||||
--->
|
||||
|
||||
This Pull Request fixes/closes #{issue_num}.
|
||||
|
||||
It changes the following:
|
||||
-
|
||||
-
|
||||
|
||||
I followed the checklist:
|
||||
- [ ] I added unittests
|
||||
- [ ] I ran `make check` without errors
|
||||
- [ ] I tested the overall application
|
||||
- [ ] I added an appropriate item to the changelog
|
||||
13
.github/dependabot.yml
vendored
|
|
@ -5,7 +5,12 @@ updates:
|
|||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
ignore:
|
||||
- dependency-name: pprof
|
||||
versions:
|
||||
- 0.4.1
|
||||
groups:
|
||||
cargo-minor:
|
||||
patterns: ["*"]
|
||||
update-types:
|
||||
- 'minor'
|
||||
cargo-patch:
|
||||
patterns: ["*"]
|
||||
update-types:
|
||||
- 'patch'
|
||||
|
|
|
|||
8
.github/stale.yml
vendored
|
|
@ -1,18 +1,18 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 90
|
||||
daysUntilStale: 180
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
daysUntilClose: 14
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- nostale
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: wontfix
|
||||
staleLabel: dormant
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
any activity half a year. It will be closed in 14 days if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
|
|
|||
23
.github/workflows/brew.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: brew update
|
||||
|
||||
on:
|
||||
# only manually
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag-name:
|
||||
required: true
|
||||
description: 'release tag'
|
||||
|
||||
jobs:
|
||||
update_brew:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@v3
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
with:
|
||||
formula-name: gitui
|
||||
# https://github.com/mislav/bump-homebrew-formula-action/issues/58
|
||||
formula-path: Formula/g/gitui.rb
|
||||
tag-name: ${{ github.event.inputs.tag-name }}
|
||||
189
.github/workflows/cd.yml
vendored
|
|
@ -2,98 +2,133 @@ name: CD
|
|||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: echo "version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: clippy
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable
|
||||
|
||||
- name: New Resolver
|
||||
run: |
|
||||
cargo install cargo-modify --force
|
||||
cargo modify new-resolver
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- name: Build
|
||||
run: cargo build
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cargo clean
|
||||
make clippy
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Setup MUSL
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
sudo apt-get -qq install musl-tools
|
||||
- name: Build
|
||||
if: matrix.os != 'ubuntu-22.04'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: cargo build
|
||||
- name: Run tests
|
||||
if: matrix.os != 'ubuntu-22.04'
|
||||
run: make test
|
||||
- name: Run clippy
|
||||
if: matrix.os != 'ubuntu-22.04'
|
||||
run: |
|
||||
cargo clean
|
||||
make clippy
|
||||
|
||||
- name: Build Release Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: make release-mac
|
||||
- name: Build Release Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: make release-linux-musl
|
||||
- name: Build Release Win
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: make release-win
|
||||
- name: Setup MUSL
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
sudo apt-get -qq install musl-tools
|
||||
|
||||
- name: Set SHA
|
||||
if: matrix.os == 'macos-latest'
|
||||
id: shasum
|
||||
run: |
|
||||
echo ::set-output name=sha::"$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')"
|
||||
- name: Setup ARM toolchain
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
|
||||
- name: Extract release notes
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
id: release_notes
|
||||
uses: ffurrer2/extract-release-notes@v1
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
files: |
|
||||
./release/*.tar.gz
|
||||
./release/*.zip
|
||||
./release/*.msi
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
|
||||
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
|
||||
|
||||
# - name: Bump personal tap formula
|
||||
# uses: mislav/bump-homebrew-formula-action@v1
|
||||
# if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
|
||||
# env:
|
||||
# COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
# with:
|
||||
# formula-name: gitui
|
||||
# homebrew-tap: extrawurst/tap
|
||||
# download-url: https://github.com/extrawurst/gitui/releases/download/${{ steps.get_version.outputs.version }}/gitui-mac.tar.gz
|
||||
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
|
||||
tar xf $GITHUB_WORKSPACE/arm.tar.xz
|
||||
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@v1
|
||||
if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
with:
|
||||
formula-name: gitui
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Build Release Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: make release-mac
|
||||
- name: Build Release Mac x86
|
||||
if: matrix.os == 'macos-latest'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
make release-mac-x86
|
||||
- name: Build Release Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: make release-linux-musl
|
||||
- name: Build Release Win
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: make release-win
|
||||
- name: Build Release Linux ARM
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
env:
|
||||
GITUI_RELEASE: 1
|
||||
run: make release-linux-arm
|
||||
|
||||
- name: Set SHA
|
||||
if: matrix.os == 'macos-latest'
|
||||
id: shasum
|
||||
run: |
|
||||
echo sha="$(shasum -a 256 ./release/gitui-mac.tar.gz | awk '{printf $1}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract release notes
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
id: release_notes
|
||||
uses: ffurrer2/extract-release-notes@v2
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||
prerelease: ${{ contains(github.ref, '-') }}
|
||||
files: |
|
||||
./release/*.tar.gz
|
||||
./release/*.zip
|
||||
./release/*.msi
|
||||
|
||||
- name: Bump homebrew-core formula
|
||||
uses: mislav/bump-homebrew-formula-action@v3
|
||||
if: "matrix.os == 'macos-latest' && !contains(github.ref, '-')" # skip prereleases
|
||||
env:
|
||||
COMMITTER_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
with:
|
||||
formula-name: gitui
|
||||
# https://github.com/mislav/bump-homebrew-formula-action/issues/58
|
||||
formula-path: Formula/g/gitui.rb
|
||||
|
|
|
|||
388
.github/workflows/ci.yml
vendored
|
|
@ -2,11 +2,11 @@ name: CI
|
|||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # run at 2 AM UTC
|
||||
- cron: "0 2 * * *"
|
||||
push:
|
||||
branches: [ '*' ]
|
||||
branches: ["*"]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
|
@ -17,151 +17,309 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
rust: [nightly, stable, '1.50']
|
||||
rust: [nightly, stable, "1.88"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
target
|
||||
key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}-${{ hashFiles('Cargo.lock') }}
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-${{ matrix.rust }}
|
||||
|
||||
- name: MacOS Workaround
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: cargo clean -p serde_derive -p thiserror
|
||||
- name: MacOS Workaround
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: cargo clean -p serde_derive -p thiserror
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
default: true
|
||||
profile: minimal
|
||||
components: clippy
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
components: clippy
|
||||
|
||||
- name: New Resolver
|
||||
if: matrix.rust != '1.50'
|
||||
run: |
|
||||
cargo install cargo-modify --force
|
||||
cargo modify new-resolver
|
||||
- name: Override rust toolchain
|
||||
run: rustup override set ${{ matrix.rust }}
|
||||
|
||||
- name: Build Debug
|
||||
run: |
|
||||
cargo build
|
||||
- name: Rustup Show
|
||||
run: rustup show
|
||||
|
||||
- name: Run tests
|
||||
run: make test
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
make clippy
|
||||
- name: Build Debug
|
||||
run: |
|
||||
cargo build
|
||||
|
||||
- name: Build Release
|
||||
run: make build-release
|
||||
- name: Run tests
|
||||
run: make test
|
||||
|
||||
- name: Binary Size (unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
ls -l ./target/release/gitui
|
||||
|
||||
- name: Binary Size (win)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
ls -l ./target/release/gitui.exe
|
||||
- name: Run clippy
|
||||
run: |
|
||||
make clippy
|
||||
|
||||
- name: Binary dependencies (mac)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
otool -L ./target/release/gitui
|
||||
- name: Build Release
|
||||
run: make build-release
|
||||
|
||||
- name: Build MSI (windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cargo install cargo-wix
|
||||
cargo wix --no-build --nocapture --output ./target/wix/gitui.msi
|
||||
ls -l ./target/wix/gitui.msi
|
||||
- name: Test Install
|
||||
run: cargo install --path "." --force --locked
|
||||
|
||||
- name: Binary Size (unix)
|
||||
if: matrix.os != 'windows-latest'
|
||||
run: |
|
||||
ls -l ./target/release/gitui
|
||||
|
||||
- name: Binary Size (win)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
ls -l ./target/release/gitui.exe
|
||||
|
||||
- name: Binary dependencies (mac)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
otool -L ./target/release/gitui
|
||||
|
||||
- name: Build MSI (windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
cargo install cargo-wix --version 0.3.3 --locked
|
||||
cargo wix --version
|
||||
cargo wix -p gitui --no-build --nocapture --output ./target/wix/gitui-win.msi
|
||||
ls -l ./target/wix/gitui-win.msi
|
||||
|
||||
build-linux-musl:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [nightly, stable, '1.50']
|
||||
rust: [nightly, stable, "1.88"]
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
profile: minimal
|
||||
default: true
|
||||
target: x86_64-unknown-linux-musl
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# TODO: remove once we depend on 1.51 as a msrv and resolver is supported by default
|
||||
- name: New Resolver
|
||||
if: matrix.rust != '1.50'
|
||||
run: |
|
||||
cargo install cargo-modify --force
|
||||
cargo modify new-resolver
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
|
||||
|
||||
- name: Setup MUSL
|
||||
run: |
|
||||
sudo apt-get -qq install musl-tools
|
||||
- name: Build Debug
|
||||
run: |
|
||||
make build-linux-musl-debug
|
||||
./target/x86_64-unknown-linux-musl/debug/gitui --version
|
||||
- name: Build Release
|
||||
run: |
|
||||
make build-linux-musl-release
|
||||
./target/x86_64-unknown-linux-musl/release/gitui --version
|
||||
ls -l ./target/x86_64-unknown-linux-musl/release/gitui
|
||||
- name: Test
|
||||
run: |
|
||||
make test-linux-musl
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
targets: x86_64-unknown-linux-musl
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
# The build would fail without manually installing the target.
|
||||
# https://github.com/dtolnay/rust-toolchain/issues/83
|
||||
- name: Manually install target
|
||||
run: rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
- name: Override rust toolchain
|
||||
run: rustup override set ${{ matrix.rust }}
|
||||
|
||||
- name: Rustup Show
|
||||
run: rustup show
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Setup MUSL
|
||||
run: |
|
||||
sudo apt-get -qq install musl-tools
|
||||
- name: Build Debug
|
||||
run: |
|
||||
make build-linux-musl-debug
|
||||
./target/x86_64-unknown-linux-musl/debug/gitui --version
|
||||
- name: Build Release
|
||||
run: |
|
||||
make build-linux-musl-release
|
||||
./target/x86_64-unknown-linux-musl/release/gitui --version
|
||||
ls -l ./target/x86_64-unknown-linux-musl/release/gitui
|
||||
- name: Test
|
||||
run: |
|
||||
make test-linux-musl
|
||||
- name: Test Install
|
||||
run: cargo install --path "." --force --locked
|
||||
|
||||
build-linux-arm:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [nightly, stable, "1.88"]
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
key: ubuntu-latest-${{ env.cache-name }}-${{ matrix.rust }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Override rust toolchain
|
||||
run: rustup override set ${{ matrix.rust }}
|
||||
|
||||
- name: Setup ARM toolchain
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
|
||||
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
|
||||
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
|
||||
|
||||
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
|
||||
tar xf $GITHUB_WORKSPACE/arm.tar.xz
|
||||
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Rustup Show
|
||||
run: rustup show
|
||||
|
||||
- name: Build Debug
|
||||
run: |
|
||||
make build-linux-arm-debug
|
||||
- name: Build Release
|
||||
run: |
|
||||
make build-linux-arm-release
|
||||
ls -l ./target/aarch64-unknown-linux-gnu/release/gitui || ls -l ./target/armv7-unknown-linux-gnueabihf/release/gitui || ls -l ./target/arm-unknown-linux-gnueabihf/release/gitui
|
||||
|
||||
build-apple-x86:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [nightly, stable, "1.88"]
|
||||
continue-on-error: ${{ matrix.rust == 'nightly' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
key: apple-x86-${{ env.cache-name }}-${{ matrix.rust }}
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Override rust toolchain
|
||||
run: rustup override set ${{ matrix.rust }}
|
||||
|
||||
- name: Setup target
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Rustup Show
|
||||
run: rustup show
|
||||
|
||||
- name: Build Debug
|
||||
run: |
|
||||
make build-apple-x86-debug
|
||||
- name: Build Release
|
||||
run: |
|
||||
make build-apple-x86-release
|
||||
ls -l ./target/x86_64-apple-darwin/release/gitui
|
||||
|
||||
linting:
|
||||
name: Lints
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
sec:
|
||||
name: Security audit
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
key: ubuntu-latest-${{ env.cache-name }}-stable
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- run: cargo fmt -- --check
|
||||
|
||||
- name: tombi install
|
||||
uses: tombi-toml/setup-tombi@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
version: '0.9.0'
|
||||
|
||||
- name: tombi check
|
||||
run: |
|
||||
tombi format --check
|
||||
|
||||
- name: cargo-deny install
|
||||
run: |
|
||||
cargo install --locked cargo-deny
|
||||
|
||||
- name: cargo-deny checks
|
||||
run: |
|
||||
cargo deny check
|
||||
|
||||
udeps:
|
||||
name: udeps
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
key: ubuntu-latest-${{ env.cache-name }}-nightly
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: build cargo-udeps
|
||||
run: cargo install --git https://github.com/est31/cargo-udeps --locked
|
||||
|
||||
- name: run cargo-udeps
|
||||
run: cargo +nightly udeps --all-targets
|
||||
|
||||
log-test:
|
||||
name: Changelog Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Extract release notes
|
||||
id: extract_release_notes
|
||||
uses: ffurrer2/extract-release-notes@v1
|
||||
with:
|
||||
release_notes_file: ./release-notes.txt
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: release-notes.txt
|
||||
path: ./release-notes.txt
|
||||
- uses: actions/checkout@v4
|
||||
- name: Extract release notes
|
||||
id: extract_release_notes
|
||||
uses: ffurrer2/extract-release-notes@v2
|
||||
with:
|
||||
release_notes_file: ./release-notes.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-notes.txt
|
||||
path: ./release-notes.txt
|
||||
|
||||
test-homebrew:
|
||||
name: Test Homebrew Formula (macOS)
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Set up Homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
|
||||
- name: Install stable Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Let Homebrew build gitui from source
|
||||
run: brew install --build-from-source gitui
|
||||
|
|
|
|||
125
.github/workflows/nightly.yml
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
name: Build Nightly Releases
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
AWS_BUCKET_NAME: s3://gitui/nightly/
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-22.04]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Restore cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
env:
|
||||
cache-name: ci
|
||||
with:
|
||||
shared-key: ${{ matrix.os }}-${{ env.cache-name }}-stable
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
|
||||
- uses: taiki-e/install-action@nextest
|
||||
|
||||
# ideally we trigger the nightly build/deploy only if the normal nightly CI finished successfully
|
||||
- name: Run tests
|
||||
if: matrix.os != 'ubuntu-22.04'
|
||||
run: make test
|
||||
- name: Run clippy
|
||||
if: matrix.os != 'ubuntu-22.04'
|
||||
run: |
|
||||
cargo clean
|
||||
make clippy
|
||||
|
||||
- name: Setup MUSL
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
sudo apt-get -qq install musl-tools
|
||||
|
||||
- name: Setup ARM toolchain
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: |
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
rustup target add armv7-unknown-linux-gnueabihf
|
||||
rustup target add arm-unknown-linux-gnueabihf
|
||||
|
||||
curl -o $GITHUB_WORKSPACE/aarch64.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu.tar.xz
|
||||
curl -o $GITHUB_WORKSPACE/arm.tar.xz https://armkeil.blob.core.windows.net/developer/Files/downloads/gnu-a/8.2-2018.08/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf.tar.xz
|
||||
|
||||
tar xf $GITHUB_WORKSPACE/aarch64.tar.xz
|
||||
tar xf $GITHUB_WORKSPACE/arm.tar.xz
|
||||
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-aarch64-linux-gnu/bin" >> $GITHUB_PATH
|
||||
echo "$GITHUB_WORKSPACE/gcc-arm-8.2-2018.08-x86_64-arm-linux-gnueabihf/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Build Release Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: make release-mac
|
||||
- name: Build Release Mac x86
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
rustup target add x86_64-apple-darwin
|
||||
make release-mac-x86
|
||||
- name: Build Release Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: make release-linux-musl
|
||||
- name: Build Release Win
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: make release-win
|
||||
- name: Build Release Linux ARM
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
run: make release-linux-arm
|
||||
|
||||
- name: Ubuntu 22.04 Upload Artifact
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
aws s3 cp ./release/gitui-linux-armv7.tar.gz $AWS_BUCKET_NAME
|
||||
aws s3 cp ./release/gitui-linux-arm.tar.gz $AWS_BUCKET_NAME
|
||||
aws s3 cp ./release/gitui-linux-aarch64.tar.gz $AWS_BUCKET_NAME
|
||||
|
||||
- name: Ubuntu Latest Upload Artifact
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
aws s3 cp ./release/gitui-linux-x86_64.tar.gz $AWS_BUCKET_NAME
|
||||
|
||||
- name: MacOS Upload Artifact
|
||||
if: matrix.os == 'macos-latest'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
aws s3 cp ./release/gitui-mac.tar.gz $AWS_BUCKET_NAME
|
||||
aws s3 cp ./release/gitui-mac-x86.tar.gz $AWS_BUCKET_NAME
|
||||
|
||||
- name: Windows Upload Artifact
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_KEY_SECRET }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
aws s3 cp ./release/gitui-win.msi $env:AWS_BUCKET_NAME
|
||||
aws s3 cp ./release/gitui-win.tar.gz $env:AWS_BUCKET_NAME
|
||||
1
.vscode/settings.json
vendored
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"workbench.settings.enableNaturalLanguageSearch": false,
|
||||
"telemetry.enableTelemetry": false,
|
||||
}
|
||||
892
CHANGELOG.md
26
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Contributing
|
||||
|
||||
We’re glad you found this document that is intended to make contributing to
|
||||
GitUI as easy as possible!
|
||||
|
||||
## Building GitUI
|
||||
|
||||
In order to build GitUI on your machine, follow the instructions in the
|
||||
[“Build” section](./README.md#build).
|
||||
|
||||
## Getting help
|
||||
|
||||
There’s a [Discord server][discord-server] you can join if you get stuck or
|
||||
don’t know where to start. People are happy to answer any questions you might
|
||||
have!
|
||||
|
||||
## Getting started
|
||||
|
||||
If you are looking for something to work on, but don’t yet know what might be a
|
||||
good first issue, you can take a look at [issues labelled with
|
||||
`good-first-issue`][good-first-issues]. They have been selected to not require
|
||||
too much context so that people not familiar with the codebase yet can still
|
||||
make a contribution.
|
||||
|
||||
[discord-server]: https://discord.gg/rZv4uxSQx3
|
||||
[good-first-issues]: https://github.com/gitui-org/gitui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
|
||||
4810
Cargo.lock
generated
155
Cargo.toml
|
|
@ -1,87 +1,110 @@
|
|||
[package]
|
||||
name = "gitui"
|
||||
version = "0.16.2"
|
||||
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
|
||||
version = "0.28.1"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
description = "blazing fast terminal-ui for git"
|
||||
edition = "2018"
|
||||
exclude = [".github/*",".vscode/*","assets/*"]
|
||||
homepage = "https://github.com/extrawurst/gitui"
|
||||
repository = "https://github.com/extrawurst/gitui"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
exclude = [".github/*", ".vscode/*", "assets/*"]
|
||||
homepage = "https://github.com/gitui-org/gitui"
|
||||
repository = "https://github.com/gitui-org/gitui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = [
|
||||
"git",
|
||||
"gui",
|
||||
"cli",
|
||||
"terminal",
|
||||
"ui",
|
||||
keywords = ["cli", "git", "gui", "terminal", "ui"]
|
||||
build = "build.rs"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"asyncgit",
|
||||
"filetreelist",
|
||||
"git2-hooks",
|
||||
"git2-testing",
|
||||
"scopetime",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
scopetime = { path = "./scopetime", version = "0.1" }
|
||||
asyncgit = { path = "./asyncgit", version = "0.16" }
|
||||
filetreelist = { path = "./filetreelist", version = "0.2" }
|
||||
crossterm = { version = "0.19", features = [ "serde" ] }
|
||||
clap = { version = "2.33", default-features = false }
|
||||
tui = { version = "0.15", default-features = false, features = ['crossterm', 'serde'] }
|
||||
bytesize = { version = "1.0", default-features = false}
|
||||
itertools = "0.10"
|
||||
rayon-core = "1.9"
|
||||
log = "0.4"
|
||||
simplelog = { version = "0.10", default-features = false }
|
||||
dirs-next = "2.0"
|
||||
crossbeam-channel = "0.5"
|
||||
scopeguard = "1.1"
|
||||
bitflags = "1.2"
|
||||
chrono = "0.4"
|
||||
backtrace = "0.3"
|
||||
ron = "0.6"
|
||||
serde = "1.0"
|
||||
anyhow = "1.0"
|
||||
unicode-width = "0.1"
|
||||
textwrap = "0.14"
|
||||
unicode-truncate = "0.2"
|
||||
unicode-segmentation = "1.7"
|
||||
easy-cast = "0.4"
|
||||
bugreport = "0.4"
|
||||
lazy_static = "1.4"
|
||||
syntect = { version = "4.5", default-features = false, features = ["metadata", "default-fancy"]}
|
||||
|
||||
[target.'cfg(all(target_family="unix",not(target_os="macos")))'.dependencies]
|
||||
which = "4.1"
|
||||
|
||||
# pprof is not available on windows
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
pprof = { version = "0.4", features = ["flamegraph"], optional = true }
|
||||
asyncgit = { path = "./asyncgit", version = "0.28.1", default-features = false }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bitflags = "2.10"
|
||||
bugreport = "0.5.1"
|
||||
bwrap = { version = "1.3", features = ["use_std"] }
|
||||
bytesize = { version = "2.3", default-features = false }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5", features = ["cargo", "env"] }
|
||||
crossbeam-channel = "0.5"
|
||||
crossterm = { version = "0.29", features = ["serde"] }
|
||||
dirs = "6.0"
|
||||
easy-cast = "0.5"
|
||||
filetreelist = { path = "./filetreelist", version = ">=0.6" }
|
||||
fuzzy-matcher = "0.3"
|
||||
gh-emoji = { version = "1.0", optional = true }
|
||||
indexmap = "2"
|
||||
itertools = "0.14"
|
||||
log = "0.4"
|
||||
notify = "8"
|
||||
notify-debouncer-mini = "0.7"
|
||||
once_cell = "1"
|
||||
parking_lot_core = "0.9"
|
||||
ratatui = { version = "0.30", default-features = false, features = [
|
||||
"crossterm",
|
||||
"serde",
|
||||
] }
|
||||
ratatui-textarea = "0.8"
|
||||
rayon-core = "1.13"
|
||||
ron = "0.12"
|
||||
scopeguard = "1.2"
|
||||
scopetime = { path = "./scopetime", version = "0.1" }
|
||||
serde = "1.0"
|
||||
shellexpand = "3.1"
|
||||
simplelog = { version = "0.12", default-features = false }
|
||||
struct-patch = "0.10"
|
||||
syntect = { version = "5.3", default-features = false, features = [
|
||||
"default-syntaxes",
|
||||
"default-themes",
|
||||
"html",
|
||||
"parsing",
|
||||
"plist-load",
|
||||
] }
|
||||
two-face = { version = "0.4.4", default-features = false }
|
||||
unicode-segmentation = "1.12"
|
||||
unicode-truncate = "2.0"
|
||||
unicode-width = "0.2"
|
||||
which = "8.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "0.7"
|
||||
env_logger = "0.11"
|
||||
git2-testing = { path = "./git2-testing" }
|
||||
insta = { version = "1.41.0", features = ["filters"] }
|
||||
pretty_assertions = "1.4"
|
||||
tempfile = "3"
|
||||
|
||||
[build-dependencies]
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[features]
|
||||
default=[]
|
||||
timing=["scopetime/enabled"]
|
||||
default = ["ghemoji", "regex-fancy", "trace-libgit", "vendor-openssl"]
|
||||
ghemoji = ["gh-emoji"]
|
||||
# regex-* features are mutually exclusive.
|
||||
regex-fancy = ["syntect/regex-fancy", "two-face/syntect-fancy"]
|
||||
regex-onig = ["syntect/regex-onig", "two-face/syntect-onig"]
|
||||
timing = ["scopetime/enabled"]
|
||||
trace-libgit = ["asyncgit/trace-libgit"]
|
||||
vendor-openssl = ["asyncgit/vendor-openssl"]
|
||||
|
||||
[workspace]
|
||||
members=[
|
||||
"asyncgit",
|
||||
"scopetime",
|
||||
"filetreelist",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z' # Optimize for size.
|
||||
codegen-units = 1
|
||||
|
||||
# make debug build as fast as release
|
||||
# usage of utf8 encoding inside tui
|
||||
# make debug build as fast as release
|
||||
# usage of utf8 encoding inside tui
|
||||
# makes their debug profile slow
|
||||
[profile.dev.package."tui"]
|
||||
[profile.dev.package."ratatui"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
[profile.release]
|
||||
opt-level = "z" # Optimize for size.
|
||||
strip = "debuginfo"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
|
|
|
|||
26
FAQ.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
|
||||
## <a name="table-of-contents"></a> Table of Contents
|
||||
|
||||
1. ["Bad Credentials" Error](#credentials)
|
||||
2. [Custom key bindings](#keybindings)
|
||||
2. [Watcher](#watcher)
|
||||
|
||||
## 1. <a name="credentials"></a> "Bad Credentials" Error <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
Some users have trouble pushing/pulling from remotes and adding their ssh-key to their ssh-agent solved the issue. The error they get is:
|
||||

|
||||
|
||||
See Github's excellent documentation for [Adding your SSH Key to the ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent)
|
||||
|
||||
Note that in some cases adding the line `ssh-add -K ~/.ssh/id_ed25519`(or whatever your key is called) to your bash init script is necessary too to survive restarts.
|
||||
|
||||
## 2. <a name="keybindings"></a> Custom key bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
If you want to use `vi`-style keys or customize your key bindings in any other fashion see the specific docs on that: [key config](./KEY_CONFIG.md)
|
||||
|
||||
## 3. <a name="watcher"></a> Watching for changes <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
By default, `gitui` polls for changes in the working directory every 5 seconds. If you supply `--watcher` as an argument, it uses a `notify`-based approach instead. This is usually faster and was for some time the default update strategy. It turned out, however, that `notify`-based updates can cause issues on some platforms, so tick-based updates seemed like a safer default.
|
||||
|
||||
See #1444 for details.
|
||||
|
|
@ -4,14 +4,48 @@ The default keys are based on arrow keys to navigate.
|
|||
|
||||
However popular demand lead to fully customizability of the key bindings.
|
||||
|
||||
On first start `gitui` will create `key_config.ron` file automatically based on the defaults.
|
||||
This file allows changing every key binding.
|
||||
Create a `key_bindings.ron` file like this:
|
||||
```
|
||||
(
|
||||
move_left: Some(( code: Char('h'), modifiers: "")),
|
||||
move_right: Some(( code: Char('l'), modifiers: "")),
|
||||
move_up: Some(( code: Char('k'), modifiers: "")),
|
||||
move_down: Some(( code: Char('j'), modifiers: "")),
|
||||
|
||||
stash_open: Some(( code: Char('l'), modifiers: "")),
|
||||
open_help: Some(( code: F(1), modifiers: "")),
|
||||
|
||||
status_reset_item: Some(( code: Char('U'), modifiers: "SHIFT")),
|
||||
)
|
||||
```
|
||||
|
||||
The config file format based on the [Ron file format](https://github.com/ron-rs/ron).
|
||||
The location of the file depends on your OS:
|
||||
* `$HOME/.config/gitui/key_config.ron` (mac)
|
||||
* `$XDG_CONFIG_HOME/gitui/key_config.ron` (linux using XDG)
|
||||
* `$HOME/.config/gitui/key_config.ron` (linux)
|
||||
* `%APPDATA%/gitui/key_config.ron` (Windows)
|
||||
* `$HOME/.config/gitui/key_bindings.ron` (mac)
|
||||
* `$XDG_CONFIG_HOME/gitui/key_bindings.ron` (linux using XDG)
|
||||
* `$HOME/.config/gitui/key_bindings.ron` (linux)
|
||||
* `%APPDATA%/gitui/key_bindings.ron` (Windows)
|
||||
|
||||
Here is a [vim style key config](vim_style_key_config.ron) with `h`, `j`, `k`, `l` to navigate. Use it to copy the content into `key_config.ron` to get vim style key bindings.
|
||||
See all possible keys to overwrite in gitui: [here](https://github.com/gitui-org/gitui/blob/master/src/keys/key_list.rs#L83)
|
||||
|
||||
Possible values for:
|
||||
* `code` are defined by the type `KeyCode` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/enum.KeyCode.html)
|
||||
* `modifiers` are defined by the type `KeyModifiers` in crossterm: [here](https://docs.rs/crossterm/latest/crossterm/event/struct.KeyModifiers.html)
|
||||
|
||||
Here is a [vim style key config](vim_style_key_config.ron) with `h`, `j`, `k`, `l` to navigate. Use it to copy the content into `key_bindings.ron` to get vim style key bindings.
|
||||
|
||||
# Key Symbols
|
||||
|
||||
Similar to the above GitUI allows you to change the way the UI visualizes key combos containing special keys like `enter`(default: `⏎`) and `shift`(default: `⇧`).
|
||||
|
||||
If we can find a file `key_symbols.ron` in the above folders we apply the overwrites in it.
|
||||
|
||||
Example content of this file looks like:
|
||||
|
||||
```
|
||||
(
|
||||
enter: Some("enter"),
|
||||
shift: Some("shift-")
|
||||
)
|
||||
```
|
||||
This example will only overwrite two symbols. Find all possible symbols to overwrite in `symbols.rs` in the type `KeySymbolsFile` ([src/keys/symbols.rs](https://github.com/gitui-org/gitui/blob/master/src/keys/symbols.rs))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stephan Dilly
|
||||
Copyright (c) 2025 gitui-org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
90
Makefile
|
|
@ -1,66 +1,120 @@
|
|||
|
||||
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug
|
||||
.PHONY: debug build-release release-linux-musl test clippy clippy-pedantic install install-debug sort
|
||||
|
||||
ARGS=-l
|
||||
# ARGS=-l -d <some_path>
|
||||
# ARGS=-l -d ~/code/extern/kubernetes
|
||||
# ARGS=-l -d ~/code/extern/linux
|
||||
# ARGS=-l -d ~/code/git-bare-test.git -w ~/code/git-bare-test
|
||||
|
||||
profile:
|
||||
cargo run --features=timing,pprof -- ${ARGS}
|
||||
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --features timing -- ${ARGS}
|
||||
|
||||
run-timing:
|
||||
cargo run --features=timing --release -- ${ARGS}
|
||||
|
||||
debug:
|
||||
RUST_BACKTRACE=true cargo run --features=timing -- ${ARGS}
|
||||
|
||||
build-release:
|
||||
cargo build --release
|
||||
cargo build --release --locked
|
||||
|
||||
release-mac: build-release
|
||||
strip target/release/gitui
|
||||
otool -L target/release/gitui
|
||||
ls -lisah target/release/gitui
|
||||
mkdir -p release
|
||||
tar -C ./target/release/ -czvf ./release/gitui-mac.tar.gz ./gitui
|
||||
ls -lisah ./release/gitui-mac.tar.gz
|
||||
|
||||
release-mac-x86: build-apple-x86-release
|
||||
strip target/x86_64-apple-darwin/release/gitui
|
||||
otool -L target/x86_64-apple-darwin/release/gitui
|
||||
ls -lisah target/x86_64-apple-darwin/release/gitui
|
||||
mkdir -p release
|
||||
tar -C ./target/x86_64-apple-darwin/release/ -czvf ./release/gitui-mac-x86.tar.gz ./gitui
|
||||
ls -lisah ./release/gitui-mac-x86.tar.gz
|
||||
|
||||
release-win: build-release
|
||||
mkdir -p release
|
||||
tar -C ./target/release/ -czvf ./release/gitui-win.tar.gz ./gitui.exe
|
||||
cargo install cargo-wix
|
||||
cargo wix --no-build --nocapture --output ./release/gitui.msi
|
||||
ls -l ./release/gitui.msi
|
||||
cargo install cargo-wix --version 0.3.3 --locked
|
||||
cargo wix -p gitui --no-build --nocapture --output ./release/gitui-win.msi
|
||||
ls -l ./release/gitui-win.msi
|
||||
|
||||
release-linux-musl: build-linux-musl-release
|
||||
strip target/x86_64-unknown-linux-musl/release/gitui
|
||||
mkdir -p release
|
||||
tar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-musl.tar.gz ./gitui
|
||||
tar -C ./target/x86_64-unknown-linux-musl/release/ -czvf ./release/gitui-linux-x86_64.tar.gz ./gitui
|
||||
|
||||
build-apple-x86-debug:
|
||||
cargo build --target=x86_64-apple-darwin
|
||||
|
||||
build-apple-x86-release:
|
||||
cargo build --release --target=x86_64-apple-darwin --locked
|
||||
|
||||
build-linux-musl-debug:
|
||||
cargo build --target=x86_64-unknown-linux-musl
|
||||
|
||||
build-linux-musl-release:
|
||||
cargo build --release --target=x86_64-unknown-linux-musl
|
||||
cargo build --release --target=x86_64-unknown-linux-musl --locked
|
||||
|
||||
test-linux-musl:
|
||||
cargo test --workspace --target=x86_64-unknown-linux-musl
|
||||
cargo nextest run --workspace --target=x86_64-unknown-linux-musl
|
||||
|
||||
release-linux-arm: build-linux-arm-release
|
||||
mkdir -p release
|
||||
|
||||
aarch64-linux-gnu-strip target/aarch64-unknown-linux-gnu/release/gitui
|
||||
arm-linux-gnueabihf-strip target/armv7-unknown-linux-gnueabihf/release/gitui
|
||||
arm-linux-gnueabihf-strip target/arm-unknown-linux-gnueabihf/release/gitui
|
||||
|
||||
tar -C ./target/aarch64-unknown-linux-gnu/release/ -czvf ./release/gitui-linux-aarch64.tar.gz ./gitui
|
||||
tar -C ./target/armv7-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-armv7.tar.gz ./gitui
|
||||
tar -C ./target/arm-unknown-linux-gnueabihf/release/ -czvf ./release/gitui-linux-arm.tar.gz ./gitui
|
||||
|
||||
build-linux-arm-debug:
|
||||
cargo build --target=aarch64-unknown-linux-gnu
|
||||
cargo build --target=armv7-unknown-linux-gnueabihf
|
||||
cargo build --target=arm-unknown-linux-gnueabihf
|
||||
|
||||
build-linux-arm-release:
|
||||
cargo build --release --target=aarch64-unknown-linux-gnu --locked
|
||||
cargo build --release --target=armv7-unknown-linux-gnueabihf --locked
|
||||
cargo build --release --target=arm-unknown-linux-gnueabihf --locked
|
||||
|
||||
test:
|
||||
cargo test --workspace
|
||||
cargo nextest run --workspace
|
||||
|
||||
fmt:
|
||||
cargo fmt -- --check
|
||||
|
||||
clippy:
|
||||
touch src/main.rs
|
||||
cargo clean -p gitui -p asyncgit -p scopetime -p filetreelist
|
||||
cargo clippy --workspace --all-features
|
||||
|
||||
clippy-nightly:
|
||||
touch src/main.rs
|
||||
cargo clean -p gitui -p asyncgit -p scopetime -p filetreelist
|
||||
cargo +nightly clippy --workspace --all-features
|
||||
|
||||
check: fmt clippy test
|
||||
check: fmt clippy test sort deny
|
||||
|
||||
check-nightly:
|
||||
cargo +nightly c
|
||||
cargo +nightly clippy --workspace --all-features
|
||||
cargo +nightly t
|
||||
|
||||
deny:
|
||||
cargo deny check
|
||||
|
||||
sort:
|
||||
tombi format --check
|
||||
|
||||
install:
|
||||
cargo install --path "." --offline
|
||||
cargo install --path "." --offline --locked
|
||||
|
||||
install-timing:
|
||||
cargo install --features=timing --path "." --offline
|
||||
cargo install --features=timing --path "." --offline --locked
|
||||
|
||||
licenses:
|
||||
cargo bundle-licenses --format toml --output THIRDPARTY.toml
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
|
|
|||
14
NIGHTLIES.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Nightlies
|
||||
|
||||
**Use with caution as these binaries are build nightly and might be broken**
|
||||
|
||||
When you find problems please report them and always mention the version that you see in the `help popup` or when running `gitui -V`
|
||||
|
||||
* [gitui-linux-aarch64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-aarch64.tar.gz)
|
||||
* [gitui-linux-arm.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-arm.tar.gz)
|
||||
* [gitui-linux-armv7.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-armv7.tar.gz)
|
||||
* [gitui-linux-x86_64.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-linux-x86_64.tar.gz)
|
||||
* [gitui-mac.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac.tar.gz)
|
||||
* [gitui-mac-x86.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-mac-x86.tar.gz)
|
||||
* [gitui-win.tar.gz](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.tar.gz)
|
||||
* [gitui-win.msi](https://gitui.s3.eu-west-1.amazonaws.com/nightly/gitui-win.msi)
|
||||
195
README.md
|
|
@ -1,23 +1,23 @@
|
|||
<h1 align="center">
|
||||
<img width="300px" src="assets/logo.png" />
|
||||
|
||||
[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![ITCH][s4]][l4] [![DISC][s5]][l5] [![TWEET][s6]][l6]
|
||||
[![CI][s0]][l0] [![crates][s1]][l1] ![MIT][s2] [![UNSAFE][s3]][l3] [![TWEET][s6]][l6] [![dep_status][s7]][l7] [![discord][s8]][l8]
|
||||
|
||||
</h1>
|
||||
|
||||
[s0]: https://github.com/extrawurst/gitui/workflows/CI/badge.svg
|
||||
[l0]: https://github.com/extrawurst/gitui/actions
|
||||
[s0]: https://github.com/gitui-org/gitui/workflows/CI/badge.svg
|
||||
[l0]: https://github.com/gitui-org/gitui/actions
|
||||
[s1]: https://img.shields.io/crates/v/gitui.svg
|
||||
[l1]: https://crates.io/crates/gitui
|
||||
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[s3]: https://img.shields.io/badge/unsafe-forbidden-success.svg
|
||||
[l3]: https://github.com/rust-secure-code/safety-dance/
|
||||
[s4]: https://img.shields.io/badge/itch.io-ok-green
|
||||
[l4]: https://extrawurst.itch.io/gitui
|
||||
[s5]: https://img.shields.io/discord/723083834811220028.svg?logo=chat
|
||||
[l5]: https://discord.gg/7TGFfuq
|
||||
[s6]: https://img.shields.io/twitter/follow/extrawurst?label=follow&style=social
|
||||
[l6]: https://twitter.com/intent/follow?screen_name=extrawurst
|
||||
[s7]: https://deps.rs/repo/github/gitui-org/gitui/status.svg
|
||||
[l7]: https://deps.rs/repo/github/gitui-org/gitui
|
||||
[s8]: https://img.shields.io/discord/1176858176897953872
|
||||
[l8]: https://discord.gg/rQNeEnMhus
|
||||
|
||||
<h5 align="center">GitUI provides you with the comfort of a git GUI but right in your terminal</h1>
|
||||
|
||||
|
|
@ -32,30 +32,35 @@
|
|||
5. [Limitations](#limitations)
|
||||
6. [Installation](#installation)
|
||||
7. [Build](#build)
|
||||
8. [Diagnostics](#diagnostics)
|
||||
9. [Color Theme](#theme)
|
||||
10. [Key Bindings](#bindings)
|
||||
11. [Sponsoring](#sponsoring)
|
||||
12. [Inspiration](#inspiration)
|
||||
8. [FAQs](#faqs)
|
||||
9. [Diagnostics](#diagnostics)
|
||||
10. [Color Theme](#theme)
|
||||
11. [Key Bindings](#bindings)
|
||||
12. [Sponsoring](#sponsoring)
|
||||
13. [Inspiration](#inspiration)
|
||||
14. [Contributing](#contributing)
|
||||
15. [Contributors](#contributors)
|
||||
|
||||
## 1. <a name="features"></a> Features <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
- Fast and intuitive **keyboard only** control
|
||||
- Context based help (**no need to memorize** tons of hot-keys)
|
||||
- Inspect, commit, and amend changes (incl. hooks: _commit-msg_/_post-commit_)
|
||||
- Inspect, commit, and amend changes (incl. hooks: *pre-commit*,*commit-msg*,*post-commit*,*prepare-commit-msg*)
|
||||
- Stage, unstage, revert and reset files, hunks and lines
|
||||
- Stashing (save, pop, apply, drop, and inspect)
|
||||
- Push/Fetch to/from remote
|
||||
- Push / Fetch to / from remote
|
||||
- Branch List (create, rename, delete, checkout, remotes)
|
||||
- Browse commit log, diff committed changes
|
||||
- Scalable terminal UI layout
|
||||
- Browse / **Search** commit log, diff committed changes
|
||||
- Responsive terminal UI
|
||||
- Async git API for fluid control
|
||||
- Submodule support
|
||||
- gpg commit signing with shortcomings (see [#97](https://github.com/gitui-org/gitui/issues/97)))
|
||||
|
||||
## 2. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
I do most of my git work in a terminal but I frequently found myself using git GUIs for some use-cases like: index, commit, diff, stash, blame and log.
|
||||
|
||||
Unfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.
|
||||
Unfortunately popular git GUIs all fail on giant repositories or become unresponsive and unusable.
|
||||
|
||||
GitUI provides you with the user experience and comfort of a git GUI but right in your terminal while being portable, fast, free and opensource.
|
||||
|
||||
|
|
@ -65,25 +70,22 @@ For a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([sl
|
|||
|
||||
| | Time | Memory (GB) | Binary (MB) | Freezes | Crashes |
|
||||
| --------- | ---------- | ----------- | ----------- | --------- | --------- |
|
||||
| `gitui` | **24 s** ✅ | **0.17** ✅ | 1.4 | **No** ✅ | **No** ✅ |
|
||||
| `lazygit` | 57 s | 2.6 | 16 | Yes | Sometimes |
|
||||
| `gitui` | **24 s** ✅ | **0.17** ✅ | 10 | **No** ✅ | **No** ✅ |
|
||||
| `lazygit` | 57 s | 2.6 | 25 | Yes | Sometimes |
|
||||
| `tig` | 4 m 20 s | 1.3 | **0.6** ✅ | Sometimes | **No** ✅ |
|
||||
|
||||
## 4. <a name="roadmap"></a> Road(map) to 1.0 <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
These are the high level goals before calling out `1.0`:
|
||||
|
||||
* log search (commit, author, sha) ([#449](https://github.com/extrawurst/gitui/issues/449),[#429](https://github.com/extrawurst/gitui/issues/429))
|
||||
* file history log ([#381](https://github.com/extrawurst/gitui/issues/381))
|
||||
* visualize branching structure in log tab ([#81](https://github.com/extrawurst/gitui/issues/81))
|
||||
* interactive rebase ([#32](https://github.com/extrawurst/gitui/issues/32))
|
||||
* notify-based change detection ([#1](https://github.com/extrawurst/gitui/issues/1))
|
||||
* visualize branching structure in log tab ([#81](https://github.com/gitui-org/gitui/issues/81))
|
||||
* interactive rebase ([#32](https://github.com/gitui-org/gitui/issues/32))
|
||||
- no git-lfs support (see [#2812](https://github.com/gitui-org/gitui/issues/2812))
|
||||
|
||||
## 5. <a name="limitations"></a> Known Limitations <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
- no support for [bare repositories](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server) (see [#100](https://github.com/extrawurst/gitui/issues/100))
|
||||
- no support for [core.hooksPath](https://git-scm.com/docs/githooks) config
|
||||
- no support for GPG signing (see [#97](https://github.com/extrawurst/gitui/issues/97))
|
||||
- no sparse repo support (see [#1226](https://github.com/gitui-org/gitui/issues/1226))
|
||||
- *credential.helper* for https needs to be **explicitly** configured (see [#800](https://github.com/gitui-org/gitui/issues/800))
|
||||
|
||||
Currently, this tool does not fully substitute the _git shell_, however both tools work well in tandem.
|
||||
|
||||
|
|
@ -93,22 +95,30 @@ All support is welcomed! Sponsors as well! ❤️
|
|||
|
||||
## 6. <a name="installation"></a> Installation <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
For the time being this product is in alpha and is not considered production ready. However, for personal use it is reasonably stable and is being used while developing itself.
|
||||
GitUI is in beta and may contain bugs and missing features. However, for personal use it is reasonably stable and is being used while developing itself.
|
||||
|
||||
### [Arch Linux](https://archlinux.org/packages/community/x86_64/gitui/)
|
||||
<a href="https://repology.org/project/gitui/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/gitui.svg" alt="Packaging status" align="right">
|
||||
</a>
|
||||
|
||||
### Various Package Managers
|
||||
|
||||
<details>
|
||||
<summary>Install Instructions</summary>
|
||||
|
||||
##### [Arch Linux](https://archlinux.org/packages/extra/x86_64/gitui/)
|
||||
|
||||
```sh
|
||||
pacman -S gitui
|
||||
```
|
||||
|
||||
### Fedora
|
||||
##### Fedora
|
||||
|
||||
```sh
|
||||
sudo dnf install gitui
|
||||
```
|
||||
|
||||
### Gentoo
|
||||
|
||||
##### Gentoo
|
||||
Available in [dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)
|
||||
|
||||
```sh
|
||||
|
|
@ -117,25 +127,49 @@ sudo emerge --sync dm9pZCAq
|
|||
sudo emerge dev-vcs/gitui::dm9pZCAq
|
||||
```
|
||||
|
||||
### Homebrew (macOS)
|
||||
##### [openSUSE](https://software.opensuse.org/package/gitui)
|
||||
|
||||
```sh
|
||||
sudo zypper install gitui
|
||||
```
|
||||
|
||||
##### Homebrew (macOS)
|
||||
|
||||
```sh
|
||||
brew install gitui
|
||||
```
|
||||
|
||||
### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)
|
||||
##### [MacPorts (macOS)](https://ports.macports.org/port/gitui/details/)
|
||||
|
||||
```sh
|
||||
port install gitui
|
||||
```
|
||||
|
||||
##### [Winget](https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/StephanDilly/gitui) (Windows)
|
||||
|
||||
```
|
||||
winget install gitui
|
||||
```
|
||||
|
||||
##### [Scoop](https://github.com/ScoopInstaller/Main/blob/master/bucket/gitui.json) (Windows)
|
||||
|
||||
```
|
||||
scoop install gitui
|
||||
```
|
||||
|
||||
### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)
|
||||
##### [Chocolatey](https://chocolatey.org/packages/gitui) (Windows)
|
||||
|
||||
```
|
||||
choco install gitui
|
||||
```
|
||||
|
||||
### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS)
|
||||
##### [Mise](https://github.com/jdx/mise)
|
||||
|
||||
```shell
|
||||
mise use -g gitui@latest
|
||||
```
|
||||
|
||||
##### [Nix](https://search.nixos.org/packages?channel=unstable&show=gitui&from=0&size=50&sort=relevance&query=gitui) (Nix/NixOS)
|
||||
|
||||
Nixpkg
|
||||
```
|
||||
|
|
@ -146,28 +180,78 @@ NixOS
|
|||
nix-env -iA nixos.gitui
|
||||
```
|
||||
|
||||
## Release Binaries
|
||||
##### [Termux](https://github.com/termux/termux-packages/tree/master/packages/gitui) (Android)
|
||||
|
||||
[Available for download in releases](https://github.com/extrawurst/gitui/releases)
|
||||
```
|
||||
pkg install gitui
|
||||
```
|
||||
|
||||
##### [Anaconda](https://anaconda.org/conda-forge/gitui)
|
||||
```
|
||||
conda install -c conda-forge gitui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### Release Binaries
|
||||
|
||||
[Available for download in releases](https://github.com/gitui-org/gitui/releases)
|
||||
|
||||
Binaries available for:
|
||||
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
#### Linux
|
||||
|
||||
- gitui-linux-x86_64.tar.gz (linux musl statically linked)
|
||||
- gitui-linux-aarch64.tar.gz (linux on 64 bit arm)
|
||||
- gitui-linux-arm.tar.gz
|
||||
- gitui-linux-armv7.tar.gz
|
||||
|
||||
All contain a single binary file
|
||||
|
||||
#### macOS
|
||||
|
||||
- gitui-mac.tar.gz (arm64)
|
||||
- gitui-mac-x86.tar.gz (intel x86)
|
||||
|
||||
#### Windows
|
||||
|
||||
- gitui-win.tar.gz (single 64bit binary)
|
||||
- gitui-win.msi (64bit Installer package)
|
||||
|
||||
### Nightly Builds
|
||||
|
||||
see [NIGHTLIES.md](./NIGHTLIES.md)
|
||||
|
||||
## 7. <a name="build"></a> Build <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
### Requirements
|
||||
|
||||
- Latest `rust` and `cargo`
|
||||
- Minimum supported `rust`/`cargo` version: `1.88`
|
||||
- See [Install Rust](https://www.rust-lang.org/tools/install)
|
||||
|
||||
- To build openssl dependency (see https://docs.rs/openssl/latest/openssl/)
|
||||
- perl >= 5.12 (strawberry perl works for windows https://strawberryperl.com/)
|
||||
- a c compiler (msvc, gcc or clang, cargo will find it)
|
||||
|
||||
- To run the complete test suite python is required (and it must be invocable as `python`)
|
||||
|
||||
### Cargo Install
|
||||
|
||||
The simplest way to start playing around with `gitui` is to have `cargo` build and install it with `cargo install gitui`
|
||||
The simplest way to start playing around with `gitui` is to have `cargo` build and install it with `cargo install gitui --locked`. If you are not familiar with rust and cargo: [Getting Started with Rust](https://doc.rust-lang.org/book/ch01-00-getting-started.html)
|
||||
|
||||
## 8. <a name="diagnostics"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
### Cargo Features
|
||||
#### trace-libgit
|
||||
enable `libgit2` tracing
|
||||
|
||||
works if `libgit2` built with `-DENABLE_TRACE=ON`
|
||||
|
||||
this feature enabled by default, to disable: `cargo install --no-default-features`
|
||||
|
||||
## 8. <a name="faqs"></a> FAQs <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
see [FAQs page](./FAQ.md)
|
||||
|
||||
## 9. <a name="diagnostics"></a> Diagnostics <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
To run with logging enabled run `gitui -l`.
|
||||
|
||||
|
|
@ -178,7 +262,7 @@ This will log to:
|
|||
- Linux: `$HOME/.cache/gitui/gitui.log`
|
||||
- Windows: `%LOCALAPPDATA%/gitui/gitui.log`
|
||||
|
||||
## 9. <a name="theme"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
## 10. <a name="theme"></a> Color Theme <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||

|
||||
|
||||
|
|
@ -186,19 +270,32 @@ This will log to:
|
|||
|
||||
However, you can customize everything to your liking: See [Themes](THEMES.md).
|
||||
|
||||
## 10. <a name="bindings"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
## 11. <a name="bindings"></a> Key Bindings <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
The key bindings can be customized: See [Key Config](KEY_CONFIG.md) on how to set them to `vim`-like bindings.
|
||||
|
||||
## 11. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
## 12. <a name="sponsoring"></a> Sponsoring <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
[](https://github.com/sponsors/extrawurst)
|
||||
[](https://www.buymeacoffee.com/extrawurst)
|
||||
|
||||
## 12. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
## 13. <a name="inspiration"></a> Inspiration <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
- [lazygit](https://github.com/jesseduffield/lazygit)
|
||||
- [tig](https://github.com/jonas/tig)
|
||||
- [GitUp](https://github.com/git-up/GitUp)
|
||||
- It would be nice to come up with a way to have the map view available in a terminal tool
|
||||
- [git-brunch](https://github.com/andys8/git-brunch)
|
||||
|
||||
## 14. <a name="contributing"></a> Contributing <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## 15. <a name="contributors"></a> Contributors <small><sup>[Top ▲](#table-of-contents)</sup></small>
|
||||
|
||||
Thanks goes to all the contributors that help make GitUI amazing! ❤️
|
||||
|
||||
Wanna become a co-maintainer? We are looking for [you](https://github.com/gitui-org/gitui/issues/2084)!
|
||||
|
||||
<a href="https://github.com/gitui-org/gitui/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=gitui-org/gitui" />
|
||||
</a>
|
||||
|
|
|
|||
84
THEMES.md
|
|
@ -3,14 +3,88 @@
|
|||
default on light terminal:
|
||||

|
||||
|
||||
to change the colors of the default theme you have to modify `theme.ron` file
|
||||
[Ron format](https://github.com/ron-rs/ron) located at config path. The path differs depending on the operating system:
|
||||
## Configuration
|
||||
|
||||
* `$HOME/Library/Application Support/gitui/theme.ron` (mac)
|
||||
To change the colors of the default theme you need to add a `theme.ron` file that contains the colors you want to override. Note that you don’t have to specify the full theme anymore (as of 0.23). Instead, it is sufficient to override just the values that you want to differ from their default values.
|
||||
|
||||
The file uses the [Ron format](https://github.com/ron-rs/ron) and is located at one of the following paths, depending on your operating system:
|
||||
|
||||
* `$HOME/.config/gitui/theme.ron` (mac)
|
||||
* `$XDG_CONFIG_HOME/gitui/theme.ron` (linux using XDG)
|
||||
* `$HOME/.config/gitui/theme.ron` (linux)
|
||||
* `%APPDATA%/gitui/theme.ron` (Windows)
|
||||
|
||||
Alternatively you may make a theme in the same directory mentioned above with and select with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron` wil use `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`
|
||||
Alternatively, you can create a theme in the same directory mentioned above and use it with the `-t` flag followed by the name of the file in the directory. E.g. If you are on linux calling `gitui -t arc.ron`, this will load the theme in `$XDG_CONFIG_HOME/gitui/arc.ron` or `$HOME/.config/gitui/arc.ron`.
|
||||
|
||||
Valid colors can be found in tui-rs' [Color](https://docs.rs/tui/0.12.0/tui/style/enum.Color.html) struct. note that rgb colors might not be supported in every terminal.
|
||||
Example theme override:
|
||||
|
||||
```ron
|
||||
(
|
||||
selection_bg: Some("Blue"),
|
||||
selection_fg: Some("#ffffff"),
|
||||
)
|
||||
```
|
||||
|
||||
Note that you need to wrap values in `Some` due to the way the overrides work (as of 0.23).
|
||||
|
||||
Notes:
|
||||
|
||||
* rgb colors might not be supported in every terminal.
|
||||
* using a color like `yellow` might appear in whatever your terminal/theme defines for `yellow`
|
||||
* valid colors can be found in ratatui's [Color](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) struct.
|
||||
* all customizable theme elements can be found in [`style.rs` in the `impl Default for Theme` block](https://github.com/gitui-org/gitui/blob/master/src/ui/style.rs#L305)
|
||||
|
||||
## Preset Themes
|
||||
|
||||
You can find preset themes by Catppuccin [here](https://github.com/catppuccin/gitui.git).
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
The syntax highlighting theme can be defined using the element `syntax`. Both [default themes of the syntect library](https://github.com/trishume/syntect/blob/7fe13c0fd53cdfa0f9fea1aa14c5ba37f81d8b71/src/dumps.rs#L215) and custom themes are supported.
|
||||
|
||||
Example syntax theme:
|
||||
```ron
|
||||
(
|
||||
syntax: Some("InspiredGitHub"),
|
||||
)
|
||||
```
|
||||
|
||||
Custom themes are located in the [configuration directory](#configuration), are using TextMate's theme format and must have a `.tmTheme` file extension. To load a custom theme, `syntax` must be set to the file name without the file extension. For example, to load [`Blackboard.tmTheme`](https://raw.githubusercontent.com/filmgirl/TextMate-Themes/refs/heads/master/Blackboard.tmTheme), place the file next to `theme.ron` and set:
|
||||
```ron
|
||||
(
|
||||
syntax: Some("Blackboard"),
|
||||
)
|
||||
```
|
||||
|
||||
[filmgirl/TextMate-Themes](https://github.com/filmgirl/TextMate-Themes) offers many [beautiful](https://inkdeep.github.io/TextMate-Themes) TextMate themes to choose from.
|
||||
|
||||
## Customizing line breaks
|
||||
|
||||
If you want to change how the line break is displayed in the diff, you can also specify `line_break` in your `theme.ron`:
|
||||
|
||||
```ron
|
||||
(
|
||||
line_break: Some("¶"),
|
||||
)
|
||||
```
|
||||
|
||||
Note that if you want to turn it off, you should use a blank string:
|
||||
|
||||
```ron
|
||||
(
|
||||
line_break: Some(""),
|
||||
)
|
||||
```
|
||||
## Customizing selection
|
||||
|
||||
By default the `selection_fg` color is used to color the text of the selected line.
|
||||
Diff line, filename, commit hashes, time and author are re-colored with `selection_fg` color.
|
||||
This can be changed by specifying the `use_selection_fg` boolean in your `theme.ron`:
|
||||
|
||||
```
|
||||
(
|
||||
use_selection_fg: Some(false),
|
||||
)
|
||||
```
|
||||
|
||||
By default, `use_selection_fg` is set to `true`.
|
||||
|
|
|
|||
BIN
assets/add-remote.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
assets/bad-credentials.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
assets/blame-goto-line.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
assets/branch-validation.gif
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
assets/commit-msg-history.gif
Normal file
|
After Width: | Height: | Size: 481 KiB |
BIN
assets/compare.gif
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
assets/delete-tag-remote.gif
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
assets/diff-empty-line.png
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
assets/discard-changes-on-checkout.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
assets/drop-multiple-stashes.gif
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
assets/emojified-commit-message.png
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
assets/fuzzy-find-branch.gif
Normal file
|
After Width: | Height: | Size: 900 KiB |
BIN
assets/fuzzy-find-matches.gif
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
assets/fuzzy-find.gif
Normal file
|
After Width: | Height: | Size: 415 KiB |
BIN
assets/gitui-signing.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/log-search.gif
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 36 KiB |
1
assets/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600" fill="none"><g clip-path="url(#a)"><mask id="c" width="438" height="484" x="38" y="57" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#D9D9D9" d="m340 541 76-39 60-52-76-43-35-8-13 42H231l-13-38-46-7-24-106 20-29v-44l85-47 101-6 48 26 67-40-60-67-150-25-189 78-31 201 116 182 185 22Z"/></mask><g stroke="#000" filter="url(#b)" mask="url(#c)"><path stroke-width="38.4" d="M290 483a183 183 0 1 0 0-366 183 183 0 0 0 0 366Z"/><path fill="#000" stroke-linejoin="round" stroke-width="12.8" d="m480 351 23-9-18-16-5 25Zm-14 36 25-4-15-20-10 24Zm-20 33 25 1-11-23-14 22Zm-26 28 24 6-6-24-18 18Zm-32 22 23 11-1-25-22 14Zm-35 16 20 15 4-25-24 10Zm-37 9 16 18 9-23-25 5Zm-39 1 13 21 13-21h-26Zm-38-6 9 23 16-18-25-5Zm-36-14 4 25 20-15-24-10Zm-33-20-1 25 23-11-22-14Zm-28-26-6 24 24-6-18-18Zm-22-32-11 23 25-1-14-22Zm-16-35-15 20 25 4-10-24Zm-9-37-18 16 23 9-5-25Zm-1-39-21 13 21 13v-26Zm6-38-23 9 18 16 5-25Zm14-36-25 4 15 20 10-24Zm20-33-25-1 11 23 14-22Zm26-28-24-6 6 24 18-18Zm32-22-23-11 1 25 22-14Zm35-16-20-15-4 25 24-10Zm37-9-16-18-9 23 25-5Zm39-1-13-21-13 21h26Zm38 6-9-23-16 18 25 5Zm36 14-4-25-20 15 24 10Zm33 20 1-25-23 11 22 14Zm28 26 6-24-24 6 18 18Zm22 32 11-23-25 1 14 22Zm16 36 15-20-25-4 10 24Zm9 36 18-16-23-9 5 25Z"/><path fill="#000" stroke-linejoin="round" stroke-width="25.6" d="m260 121 30 30 30-30h-60Zm191 95-19 38 38 19-19-57Zm-32 211-41-6-7 41 48-35Zm-210 35-7-41-41 6 48 35Zm-99-189 38-19-19-38-19 57Z"/></g></g><g filter="url(#d)"><path fill="#000" d="M305 307h81v7h-18v71l-35-6a98 98 0 0 1-92-5c-24-16-36-41-36-75 0-26 8-47 23-63 16-16 37-24 63-24 15 0 28 3 41 8l27-5 1 60h-9c-7-36-24-54-51-54h-7c-29 4-44 30-44 80 0 16 1 30 4 42 7 24 22 36 45 36 12 0 22-4 31-10v-55h-24v-7Zm157 71h3c2 0 4 3 4 7h-70v-7l6-1c7-1 10-6 10-14v-92h-16v-8h52v115h11Zm-8-152c0 4-1 7-3 10-4 7-10 11-18 11-4 0-7-1-10-3-8-4-11-10-11-18l2-10c4-7 10-11 19-11 3 0 7 1 10 3 7 4 11 10 11 18Zm24 46v-7h20v-27l36-6v33h37v7h-37v86l1 7c1 10 5 15 12 15l4-1c7-1 13-10 16-25l8 1c-1 8-4 14-6 19-6 9-16 14-30 14h-7c-23-3-34-18-34-45v-71h-20Zm101-49v-8h85v8h-22v97c0 11 0 20 2 28 5 21 16 31 36 31 30 0 45-19 45-59v-97h-22v-8h55v8h-22v96c0 13-2 25-5 34-8 23-27 35-57 35-48 0-72-22-72-65V223h-23Zm271 162h-85v-8h23V223h-23v-8h85v8h-22v154h15c4 1 6 3 7 8Z"/></g><defs><filter id="b" width="455.7" height="455.7" x="62" y="72" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><filter id="d" width="653.4" height="191.2" x="201" y="201" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="2"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_2"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_2" result="shape"/></filter><clipPath id="a"><path fill="#fff" d="M64 74h452v452H64z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/multiline-texteditor.gif
Normal file
|
After Width: | Height: | Size: 1 MiB |
1
assets/options.drawio
Normal file
|
|
@ -0,0 +1 @@
|
|||
<mxfile host="app.diagrams.net" modified="2021-08-17T21:58:53.216Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" etag="DR-vNI6rA-1d9_EpYLQC" version="14.9.7" type="device"><diagram id="cIq82w5ce00BbVejkL92" name="Page-1">5Zldc6IwFIZ/DZfrEJCvS0Xb7exHZ9bd6WxvdlIIkGlMmBir3V+/iQQVg+12Bqm7eqHwkkB43nMCJ1puPF9fc1gWX1iKiOXY6dpyJ5bjABCG8kcpz5Xi+0El5BynutFOmOHfSIu2Vpc4RYtGQ8EYEbhsigmjFCWioUHO2arZLGOkedUS5sgQZgkkpnqHU1FUaugEO/0jwnlRXxn4UXVkDuvG+k4WBUzZak9yp5Ybc8ZEtTVfx4goeDWXqt/VkaPbgXFExd90KLNx+X10/wl4v+6/5fx28jMVH/RZniBZ6hvWgxXPNQHOljRF6iS25Y5XBRZoVsJEHV1Jz6VWiDmRe0Bu6tMhLtD66DjB9u5l2CA2R4I/yyZ1h0B30REDvGDgVcpq54AbaazFHn0n1CLUrufbs+/AyA3N5g2cnPPj5Hh+g5Njm5S2wbdPCYTeiSi5BqVZIUPesX9QwWHyKOkcUpP3L4c1hgTnVCoEZWpXgcEyCUdanuM0VT3GTcgZJiRmhPHNudxs8+mIrn9A17UNutE2MBtBeKoYHBp0JyiDSyKOQd2Lt4Xg7BHVrCij6ACflmofEokJ8ZecaAvwpjsduDC03RrxCz74bVPBqVzwDBfuFInFhoR8DhWQ5vKB1b0lr6VGL4Y4fjBwDhwJTUcA6Dc1fMOUm5wyjv7nzBgOgteN6DU1AsOFmNENccf+jKnKCh/OFQ76sCi3HC4iS1zbNCfs05zQMGd4UdnRZkCv2REZBmDFqljSRymTKj8uJx+clvepPu2oC8w9P+wT8D/jhGhxoNeEAGbxORNQLM8zDTL5KJvpIQ07cKTtTQq8e40BzEJ3gmVNdamO+O/viFlU35YCM3r+aaL2CXxAZMx4ivjBNbtwzIsOZzUQvb9jZoloWPWmxaIDdzoB11zdcD2TWuva0fBk0MwSLt6U0moAkKby/SiXm0xORfZyt5pky2BW7032DZVf9ePDtrzxNRYJoxmWvWJ1aLMQpba+oWTJF/gJWd7kWAp1uxzVZm8nJgaDenl962PLGiDwt9PavpXuyaxsqQMJWxwtxs9u7Q8EzewYAq9tBbrXOcUs32YrGeFFFezW1LGiqTUC1vTKGodWeKWUUWyF3k7pK9xPMD+1OxBEfTpg1m9f4VOFfzD4p+DWL94vh3c3cOXu7i+uzbG9Pwrd6R8=</diagram></mxfile>
|
||||
BIN
assets/options.gif
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
assets/popup-stacking.gif
Normal file
|
After Width: | Height: | Size: 781 KiB |
BIN
assets/rebase.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
assets/reset_in_log.gif
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/revert-commit.gif
Normal file
|
After Width: | Height: | Size: 682 KiB |
BIN
assets/reword.gif
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
assets/submodules.gif
Normal file
|
After Width: | Height: | Size: 640 KiB |
BIN
assets/syntax-highlighting-blame.png
Normal file
|
After Width: | Height: | Size: 750 KiB |
BIN
assets/tag-annotation.gif
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
assets/termux-android.jpg
Normal file
|
After Width: | Height: | Size: 641 KiB |
|
|
@ -1,33 +1,52 @@
|
|||
[package]
|
||||
name = "asyncgit"
|
||||
version = "0.16.3"
|
||||
authors = ["Stephan Dilly <dilly.stephan@gmail.com>"]
|
||||
edition = "2018"
|
||||
version = "0.28.1"
|
||||
authors = ["extrawurst <mail@rusticorn.com>"]
|
||||
edition = "2021"
|
||||
description = "allow using git2 in a asynchronous context"
|
||||
homepage = "https://github.com/extrawurst/gitui"
|
||||
repository = "https://github.com/extrawurst/gitui"
|
||||
homepage = "https://github.com/gitui-org/gitui"
|
||||
repository = "https://github.com/gitui-org/gitui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
categories = ["concurrency","asynchronous"]
|
||||
categories = ["asynchronous", "concurrency"]
|
||||
keywords = ["git"]
|
||||
|
||||
[dependencies]
|
||||
scopetime = { path = "../scopetime", version = "0.1" }
|
||||
git2 = "0.13"
|
||||
# pinning to vendored openssl, using the git2 feature this gets lost with new resolver
|
||||
openssl-sys = { version = '0.9', features= ["vendored"] }
|
||||
# git2 = { path = "../../github/git2-rs", features = ["vendored-openssl"]}
|
||||
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="513a8c9", features = ["vendored-openssl"]}
|
||||
rayon-core = "1.9"
|
||||
bitflags = "2"
|
||||
crossbeam-channel = "0.5"
|
||||
dirs = "6.0"
|
||||
easy-cast = "0.5"
|
||||
fuzzy-matcher = "0.3"
|
||||
git2 = "0.20"
|
||||
git2-hooks = { path = "../git2-hooks", version = "0.7" }
|
||||
gix = { version = "0.78.0", default-features = false, features = [
|
||||
"mailmap",
|
||||
"max-performance",
|
||||
"revision",
|
||||
"status",
|
||||
] }
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
url = "2.2"
|
||||
unicode-truncate = "0.2.0"
|
||||
easy-cast = "0.4"
|
||||
# git2 = { path = "../../extern/git2-rs", features = ["vendored-openssl"]}
|
||||
# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="fc13dcc", features = ["vendored-openssl"]}
|
||||
# pinning to vendored openssl, using the git2 feature this gets lost with new resolver
|
||||
openssl-sys = { version = "0.9", features = ["vendored"], optional = true }
|
||||
rayon = "1.11"
|
||||
rayon-core = "1.13"
|
||||
scopetime = { path = "../scopetime", version = "0.1" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
ssh-key = { version = "0.6.7", features = ["crypto", "encryption"] }
|
||||
thiserror = "2.0"
|
||||
unicode-truncate = "2.0"
|
||||
url = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.2"
|
||||
env_logger = "0.11"
|
||||
invalidstring = { path = "../invalidstring", version = "0.1" }
|
||||
serial_test = "0.5.1"
|
||||
pretty_assertions = "0.7"
|
||||
pretty_assertions = "1.4"
|
||||
serial_test = "3.3"
|
||||
tempfile = "3"
|
||||
|
||||
[features]
|
||||
default = ["trace-libgit"]
|
||||
trace-libgit = []
|
||||
vendor-openssl = ["openssl-sys"]
|
||||
|
|
|
|||
|
|
@ -4,206 +4,299 @@
|
|||
|
||||
use crate::error::Result;
|
||||
use crossbeam_channel::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
/// Passed to `AsyncJob::run` allowing sending intermediate progress notifications
|
||||
pub struct RunParams<
|
||||
T: Copy + Send,
|
||||
P: Clone + Send + Sync + PartialEq,
|
||||
> {
|
||||
sender: Sender<T>,
|
||||
progress: Arc<RwLock<P>>,
|
||||
}
|
||||
|
||||
impl<T: Copy + Send, P: Clone + Send + Sync + PartialEq>
|
||||
RunParams<T, P>
|
||||
{
|
||||
/// send an intermediate update notification.
|
||||
/// do not confuse this with the return value of `run`.
|
||||
/// `send` should only be used about progress notifications
|
||||
/// and not for the final notification indicating the end of the async job.
|
||||
/// see `run` for more info
|
||||
pub fn send(&self, notification: T) -> Result<()> {
|
||||
self.sender.send(notification)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// set the current progress
|
||||
pub fn set_progress(&self, p: P) -> Result<bool> {
|
||||
Ok(if *self.progress.read()? == p {
|
||||
false
|
||||
} else {
|
||||
*(self.progress.write()?) = p;
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// trait that defines an async task we can run on a threadpool
|
||||
pub trait AsyncJob: Send + Sync + Clone {
|
||||
/// can run a synchronous time intensive task
|
||||
fn run(&mut self);
|
||||
/// defines what notification type is used to communicate outside
|
||||
type Notification: Copy + Send;
|
||||
/// type of progress
|
||||
type Progress: Clone + Default + Send + Sync + PartialEq;
|
||||
|
||||
/// can run a synchronous time intensive task.
|
||||
/// the returned notification is used to tell interested parties
|
||||
/// that the job finished and the job can be access via `take_last`.
|
||||
/// prior to this final notification it is not safe to assume `take_last`
|
||||
/// will already return the correct job
|
||||
fn run(
|
||||
&mut self,
|
||||
params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification>;
|
||||
|
||||
/// allows observers to get intermediate progress status if the job customizes it
|
||||
/// by default this will be returning `Self::Progress::default()`
|
||||
fn get_progress(&self) -> Self::Progress {
|
||||
Self::Progress::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction for a FIFO task queue that will only queue up **one** `next` job.
|
||||
/// It keeps overwriting the next job until it is actually taken to be processed
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AsyncSingleJob<J: AsyncJob, T: Copy + Send + 'static> {
|
||||
next: Arc<Mutex<Option<J>>>,
|
||||
last: Arc<Mutex<Option<J>>>,
|
||||
sender: Sender<T>,
|
||||
pending: Arc<Mutex<()>>,
|
||||
notification: T,
|
||||
pub struct AsyncSingleJob<J: AsyncJob> {
|
||||
next: Arc<Mutex<Option<J>>>,
|
||||
last: Arc<Mutex<Option<J>>>,
|
||||
progress: Arc<RwLock<J::Progress>>,
|
||||
sender: Sender<J::Notification>,
|
||||
pending: Arc<Mutex<()>>,
|
||||
}
|
||||
|
||||
impl<J: 'static + AsyncJob, T: Copy + Send + 'static>
|
||||
AsyncSingleJob<J, T>
|
||||
{
|
||||
///
|
||||
pub fn new(sender: Sender<T>, value: T) -> Self {
|
||||
Self {
|
||||
next: Arc::new(Mutex::new(None)),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
pending: Arc::new(Mutex::new(())),
|
||||
notification: value,
|
||||
sender,
|
||||
}
|
||||
}
|
||||
impl<J: 'static + AsyncJob> AsyncSingleJob<J> {
|
||||
///
|
||||
pub fn new(sender: Sender<J::Notification>) -> Self {
|
||||
Self {
|
||||
next: Arc::new(Mutex::new(None)),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
pending: Arc::new(Mutex::new(())),
|
||||
progress: Arc::new(RwLock::new(J::Progress::default())),
|
||||
sender,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.try_lock().is_err()
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.try_lock().is_err()
|
||||
}
|
||||
|
||||
/// makes sure `next` is cleared and returns `true` if it actually canceled something
|
||||
pub fn cancel(&mut self) -> bool {
|
||||
if let Ok(mut next) = self.next.lock() {
|
||||
if next.is_some() {
|
||||
*next = None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/// makes sure `next` is cleared and returns `true` if it actually canceled something
|
||||
pub fn cancel(&self) -> bool {
|
||||
if let Ok(mut next) = self.next.lock() {
|
||||
if next.is_some() {
|
||||
*next = None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// take out last finished job
|
||||
pub fn take_last(&self) -> Option<J> {
|
||||
if let Ok(mut last) = self.last.lock() {
|
||||
last.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// take out last finished job
|
||||
pub fn take_last(&self) -> Option<J> {
|
||||
self.last.lock().map_or(None, |mut last| last.take())
|
||||
}
|
||||
|
||||
/// spawns `task` if nothing is running currently, otherwise schedules as `next` overwriting if `next` was set before
|
||||
pub fn spawn(&mut self, task: J) -> bool {
|
||||
self.schedule_next(task);
|
||||
self.check_for_job()
|
||||
}
|
||||
/// spawns `task` if nothing is running currently,
|
||||
/// otherwise schedules as `next` overwriting if `next` was set before.
|
||||
/// return `true` if the new task gets started right away.
|
||||
pub fn spawn(&self, task: J) -> bool {
|
||||
self.schedule_next(task);
|
||||
self.check_for_job()
|
||||
}
|
||||
|
||||
fn check_for_job(&self) -> bool {
|
||||
if self.is_pending() {
|
||||
return false;
|
||||
}
|
||||
///
|
||||
pub fn progress(&self) -> Option<J::Progress> {
|
||||
self.progress.read().ok().map(|d| (*d).clone())
|
||||
}
|
||||
|
||||
if let Some(task) = self.take_next() {
|
||||
let self_arc = self.clone();
|
||||
fn check_for_job(&self) -> bool {
|
||||
if self.is_pending() {
|
||||
return false;
|
||||
}
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
if let Err(e) = self_arc.run_job(task) {
|
||||
log::error!("async job error: {}", e);
|
||||
}
|
||||
});
|
||||
if let Some(task) = self.take_next() {
|
||||
let self_clone = (*self).clone();
|
||||
rayon_core::spawn(move || {
|
||||
if let Err(e) = self_clone.run_job(task) {
|
||||
log::error!("async job error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn run_job(&self, mut task: J) -> Result<()> {
|
||||
//limit the pending scope
|
||||
{
|
||||
let _pending = self.pending.lock()?;
|
||||
fn run_job(&self, mut task: J) -> Result<()> {
|
||||
//limit the pending scope
|
||||
{
|
||||
let _pending = self.pending.lock()?;
|
||||
|
||||
task.run();
|
||||
let notification = task.run(RunParams {
|
||||
progress: self.progress.clone(),
|
||||
sender: self.sender.clone(),
|
||||
})?;
|
||||
|
||||
if let Ok(mut last) = self.last.lock() {
|
||||
*last = Some(task);
|
||||
}
|
||||
if let Ok(mut last) = self.last.lock() {
|
||||
*last = Some(task);
|
||||
}
|
||||
|
||||
self.sender.send(self.notification)?;
|
||||
}
|
||||
self.sender.send(notification)?;
|
||||
}
|
||||
|
||||
self.check_for_job();
|
||||
self.check_for_job();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schedule_next(&mut self, task: J) {
|
||||
if let Ok(mut next) = self.next.lock() {
|
||||
*next = Some(task);
|
||||
}
|
||||
}
|
||||
fn schedule_next(&self, task: J) {
|
||||
if let Ok(mut next) = self.next.lock() {
|
||||
*next = Some(task);
|
||||
}
|
||||
}
|
||||
|
||||
fn take_next(&self) -> Option<J> {
|
||||
if let Ok(mut next) = self.next.lock() {
|
||||
next.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn take_next(&self) -> Option<J> {
|
||||
self.next.lock().map_or(None, |mut next| next.take())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crossbeam_channel::unbounded;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{
|
||||
sync::atomic::AtomicU32, thread::sleep, time::Duration,
|
||||
};
|
||||
use super::*;
|
||||
use crossbeam_channel::unbounded;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{
|
||||
sync::atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestJob {
|
||||
v: Arc<AtomicU32>,
|
||||
value_to_add: u32,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct TestJob {
|
||||
v: Arc<AtomicU32>,
|
||||
finish: Arc<AtomicBool>,
|
||||
value_to_add: u32,
|
||||
}
|
||||
|
||||
impl AsyncJob for TestJob {
|
||||
fn run(&mut self) {
|
||||
sleep(Duration::from_millis(100));
|
||||
type TestNotification = ();
|
||||
|
||||
self.v.fetch_add(
|
||||
self.value_to_add,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
}
|
||||
impl AsyncJob for TestJob {
|
||||
type Notification = TestNotification;
|
||||
type Progress = ();
|
||||
|
||||
type Notificaton = ();
|
||||
fn run(
|
||||
&mut self,
|
||||
_params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification> {
|
||||
println!("[job] wait");
|
||||
|
||||
#[test]
|
||||
fn test_overwrite() {
|
||||
let (sender, receiver) = unbounded();
|
||||
while !self.finish.load(Ordering::SeqCst) {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
|
||||
let mut job: AsyncSingleJob<TestJob, Notificaton> =
|
||||
AsyncSingleJob::new(sender, ());
|
||||
println!("[job] sleep");
|
||||
|
||||
let task = TestJob {
|
||||
v: Arc::new(AtomicU32::new(1)),
|
||||
value_to_add: 1,
|
||||
};
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
|
||||
assert!(job.spawn(task.clone()));
|
||||
sleep(Duration::from_millis(1));
|
||||
for _ in 0..5 {
|
||||
assert!(!job.spawn(task.clone()));
|
||||
}
|
||||
println!("[job] done sleeping");
|
||||
|
||||
let _foo = receiver.recv().unwrap();
|
||||
let _foo = receiver.recv().unwrap();
|
||||
assert!(receiver.is_empty());
|
||||
let res =
|
||||
self.v.fetch_add(self.value_to_add, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(
|
||||
task.v.load(std::sync::atomic::Ordering::Relaxed),
|
||||
3
|
||||
);
|
||||
}
|
||||
println!("[job] value: {res}");
|
||||
|
||||
#[test]
|
||||
fn test_cancel() {
|
||||
let (sender, receiver) = unbounded();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut job: AsyncSingleJob<TestJob, Notificaton> =
|
||||
AsyncSingleJob::new(sender, ());
|
||||
#[test]
|
||||
fn test_overwrite() {
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let task = TestJob {
|
||||
v: Arc::new(AtomicU32::new(1)),
|
||||
value_to_add: 1,
|
||||
};
|
||||
let job: AsyncSingleJob<TestJob> =
|
||||
AsyncSingleJob::new(sender);
|
||||
|
||||
assert!(job.spawn(task.clone()));
|
||||
sleep(Duration::from_millis(1));
|
||||
let task = TestJob {
|
||||
v: Arc::new(AtomicU32::new(1)),
|
||||
finish: Arc::new(AtomicBool::new(false)),
|
||||
value_to_add: 1,
|
||||
};
|
||||
|
||||
for _ in 0..5 {
|
||||
assert!(!job.spawn(task.clone()));
|
||||
}
|
||||
assert!(job.cancel());
|
||||
assert!(job.spawn(task.clone()));
|
||||
task.finish.store(true, Ordering::SeqCst);
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
let _foo = receiver.recv().unwrap();
|
||||
for _ in 0..5 {
|
||||
println!("spawn");
|
||||
assert!(!job.spawn(task.clone()));
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
task.v.load(std::sync::atomic::Ordering::Relaxed),
|
||||
2
|
||||
);
|
||||
}
|
||||
println!("recv");
|
||||
receiver.recv().unwrap();
|
||||
receiver.recv().unwrap();
|
||||
assert!(receiver.is_empty());
|
||||
|
||||
assert_eq!(
|
||||
task.v.load(std::sync::atomic::Ordering::SeqCst),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
fn wait_for_job(job: &AsyncSingleJob<TestJob>) {
|
||||
while job.is_pending() {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cancel() {
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
let job: AsyncSingleJob<TestJob> =
|
||||
AsyncSingleJob::new(sender);
|
||||
|
||||
let task = TestJob {
|
||||
v: Arc::new(AtomicU32::new(1)),
|
||||
finish: Arc::new(AtomicBool::new(false)),
|
||||
value_to_add: 1,
|
||||
};
|
||||
|
||||
assert!(job.spawn(task.clone()));
|
||||
task.finish.store(true, Ordering::SeqCst);
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
for _ in 0..5 {
|
||||
println!("spawn");
|
||||
assert!(!job.spawn(task.clone()));
|
||||
}
|
||||
|
||||
println!("cancel");
|
||||
assert!(job.cancel());
|
||||
|
||||
task.finish.store(true, Ordering::SeqCst);
|
||||
|
||||
wait_for_job(&job);
|
||||
|
||||
println!("recv");
|
||||
receiver.recv().unwrap();
|
||||
println!("received");
|
||||
|
||||
assert_eq!(
|
||||
task.v.load(std::sync::atomic::Ordering::SeqCst),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,179 +1,188 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{self, FileBlame},
|
||||
AsyncGitNotification, CWD,
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{self, CommitId, FileBlame, RepoPath},
|
||||
AsyncGitNotification,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Hash, Clone, PartialEq)]
|
||||
#[derive(Hash, Clone, PartialEq, Eq)]
|
||||
pub struct BlameParams {
|
||||
/// path to the file to blame
|
||||
pub file_path: String,
|
||||
/// path to the file to blame
|
||||
pub file_path: String,
|
||||
/// blame at a specific revision
|
||||
pub commit_id: Option<CommitId>,
|
||||
}
|
||||
|
||||
struct Request<R, A>(R, Option<A>);
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct LastResult<P, R> {
|
||||
params: P,
|
||||
hash: u64,
|
||||
result: R,
|
||||
params: P,
|
||||
result: R,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct AsyncBlame {
|
||||
current: Arc<Mutex<Request<u64, FileBlame>>>,
|
||||
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
current: Arc<Mutex<Request<u64, FileBlame>>>,
|
||||
last: Arc<Mutex<Option<LastResult<BlameParams, FileBlame>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncBlame {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last(
|
||||
&mut self,
|
||||
) -> Result<Option<(BlameParams, FileBlame)>> {
|
||||
let last = self.last.lock()?;
|
||||
///
|
||||
pub fn last(&self) -> Result<Option<(BlameParams, FileBlame)>> {
|
||||
let last = self.last.lock()?;
|
||||
|
||||
Ok(last.clone().map(|last_result| {
|
||||
(last_result.params, last_result.result)
|
||||
}))
|
||||
}
|
||||
Ok(last.clone().map(|last_result| {
|
||||
(last_result.params, last_result.result)
|
||||
}))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
if let Ok(Some(param)) = self.get_last_param() {
|
||||
self.clear_current()?;
|
||||
self.request(param)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
///
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
if let Ok(Some(param)) = self.get_last_param() {
|
||||
self.clear_current()?;
|
||||
self.request(param)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(
|
||||
&mut self,
|
||||
params: BlameParams,
|
||||
) -> Result<Option<FileBlame>> {
|
||||
log::trace!("request");
|
||||
///
|
||||
pub fn request(
|
||||
&self,
|
||||
params: BlameParams,
|
||||
) -> Result<Option<FileBlame>> {
|
||||
log::trace!("request");
|
||||
|
||||
let hash = hash(¶ms);
|
||||
let hash = hash(¶ms);
|
||||
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
|
||||
if current.0 == hash {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
if current.0 == hash {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
|
||||
current.0 = hash;
|
||||
current.1 = None;
|
||||
}
|
||||
current.0 = hash;
|
||||
current.1 = None;
|
||||
}
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let repo = self.repo.clone();
|
||||
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
let notify = Self::get_blame_helper(
|
||||
params,
|
||||
&arc_last,
|
||||
&arc_current,
|
||||
hash,
|
||||
);
|
||||
rayon_core::spawn(move || {
|
||||
let notify = Self::get_blame_helper(
|
||||
&repo,
|
||||
params,
|
||||
&arc_last,
|
||||
&arc_current,
|
||||
hash,
|
||||
);
|
||||
|
||||
let notify = match notify {
|
||||
Err(err) => {
|
||||
log::error!("get_blame_helper error: {}", err);
|
||||
true
|
||||
}
|
||||
Ok(notify) => notify,
|
||||
};
|
||||
let notify = match notify {
|
||||
Err(err) => {
|
||||
log::error!("get_blame_helper error: {err}");
|
||||
true
|
||||
}
|
||||
Ok(notify) => notify,
|
||||
};
|
||||
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
|
||||
sender
|
||||
.send(if notify {
|
||||
AsyncGitNotification::Blame
|
||||
} else {
|
||||
AsyncGitNotification::FinishUnchanged
|
||||
})
|
||||
.expect("error sending blame");
|
||||
});
|
||||
sender
|
||||
.send(if notify {
|
||||
AsyncGitNotification::Blame
|
||||
} else {
|
||||
AsyncGitNotification::FinishUnchanged
|
||||
})
|
||||
.expect("error sending blame");
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_blame_helper(
|
||||
params: BlameParams,
|
||||
arc_last: &Arc<
|
||||
Mutex<Option<LastResult<BlameParams, FileBlame>>>,
|
||||
>,
|
||||
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
|
||||
hash: u64,
|
||||
) -> Result<bool> {
|
||||
let file_blame =
|
||||
sync::blame::blame_file(CWD, ¶ms.file_path)?;
|
||||
fn get_blame_helper(
|
||||
repo_path: &RepoPath,
|
||||
params: BlameParams,
|
||||
arc_last: &Arc<
|
||||
Mutex<Option<LastResult<BlameParams, FileBlame>>>,
|
||||
>,
|
||||
arc_current: &Arc<Mutex<Request<u64, FileBlame>>>,
|
||||
hash: u64,
|
||||
) -> Result<bool> {
|
||||
let file_blame = sync::blame::blame_file(
|
||||
repo_path,
|
||||
¶ms.file_path,
|
||||
params.commit_id,
|
||||
)?;
|
||||
|
||||
let mut notify = false;
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash {
|
||||
current.1 = Some(file_blame.clone());
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
let mut notify = false;
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash {
|
||||
current.1 = Some(file_blame.clone());
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = Some(LastResult {
|
||||
result: file_blame,
|
||||
hash,
|
||||
params,
|
||||
});
|
||||
}
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = Some(LastResult {
|
||||
result: file_blame,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(notify)
|
||||
}
|
||||
Ok(notify)
|
||||
}
|
||||
|
||||
fn get_last_param(&self) -> Result<Option<BlameParams>> {
|
||||
Ok(self
|
||||
.last
|
||||
.lock()?
|
||||
.clone()
|
||||
.map(|last_result| last_result.params))
|
||||
}
|
||||
fn get_last_param(&self) -> Result<Option<BlameParams>> {
|
||||
Ok(self
|
||||
.last
|
||||
.lock()?
|
||||
.clone()
|
||||
.map(|last_result| last_result.params))
|
||||
}
|
||||
|
||||
fn clear_current(&mut self) -> Result<()> {
|
||||
let mut current = self.current.lock()?;
|
||||
current.0 = 0;
|
||||
current.1 = None;
|
||||
Ok(())
|
||||
}
|
||||
fn clear_current(&self) -> Result<()> {
|
||||
let mut current = self.current.lock()?;
|
||||
current.0 = 0;
|
||||
current.1 = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
77
asyncgit/src/branches.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use crate::{
|
||||
asyncjob::{AsyncJob, RunParams},
|
||||
error::Result,
|
||||
sync::{branch::get_branches_info, BranchInfo, RepoPath},
|
||||
AsyncGitNotification,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
enum JobState {
|
||||
Request {
|
||||
local_branches: bool,
|
||||
repo: RepoPath,
|
||||
},
|
||||
Response(Result<Vec<BranchInfo>>),
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AsyncBranchesJob {
|
||||
state: Arc<Mutex<Option<JobState>>>,
|
||||
}
|
||||
|
||||
///
|
||||
impl AsyncBranchesJob {
|
||||
///
|
||||
pub fn new(repo: RepoPath, local_branches: bool) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(Some(JobState::Request {
|
||||
repo,
|
||||
local_branches,
|
||||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn result(&self) -> Option<Result<Vec<BranchInfo>>> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
if let Some(state) = state.take() {
|
||||
return match state {
|
||||
JobState::Request { .. } => None,
|
||||
JobState::Response(result) => Some(result),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncJob for AsyncBranchesJob {
|
||||
type Notification = AsyncGitNotification;
|
||||
type Progress = ();
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
_params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
*state = state.take().map(|state| match state {
|
||||
JobState::Request {
|
||||
local_branches,
|
||||
repo,
|
||||
} => {
|
||||
let branches =
|
||||
get_branches_info(&repo, local_branches);
|
||||
|
||||
JobState::Response(branches)
|
||||
}
|
||||
JobState::Response(result) => {
|
||||
JobState::Response(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(AsyncGitNotification::Branches)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,47 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
sync::{self, branch::get_branch_name},
|
||||
error::Result,
|
||||
sync::{self, branch::get_branch_name, RepoPathRef},
|
||||
};
|
||||
use sync::Head;
|
||||
|
||||
///
|
||||
pub struct BranchName {
|
||||
last_result: Option<(Head, String)>,
|
||||
repo_path: String,
|
||||
last_result: Option<(Head, String)>,
|
||||
repo: RepoPathRef,
|
||||
}
|
||||
|
||||
impl BranchName {
|
||||
///
|
||||
pub fn new(path: &str) -> Self {
|
||||
Self {
|
||||
repo_path: path.to_string(),
|
||||
last_result: None,
|
||||
}
|
||||
}
|
||||
///
|
||||
pub const fn new(repo: RepoPathRef) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
last_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn lookup(&mut self) -> Result<String> {
|
||||
let current_head =
|
||||
sync::get_head_tuple(self.repo_path.as_str())?;
|
||||
///
|
||||
pub fn lookup(&mut self) -> Result<String> {
|
||||
let current_head = sync::get_head_tuple(&self.repo.borrow())?;
|
||||
|
||||
if let Some((last_head, branch_name)) =
|
||||
self.last_result.as_ref()
|
||||
{
|
||||
if *last_head == current_head {
|
||||
return Ok(branch_name.clone());
|
||||
}
|
||||
}
|
||||
if let Some((last_head, branch_name)) =
|
||||
self.last_result.as_ref()
|
||||
{
|
||||
if *last_head == current_head {
|
||||
return Ok(branch_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
self.fetch(current_head)
|
||||
}
|
||||
self.fetch(current_head)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last(&self) -> Option<String> {
|
||||
self.last_result.as_ref().map(|last| last.1.clone())
|
||||
}
|
||||
///
|
||||
pub fn last(&self) -> Option<String> {
|
||||
self.last_result.as_ref().map(|last| last.1.clone())
|
||||
}
|
||||
|
||||
fn fetch(&mut self, head: Head) -> Result<String> {
|
||||
let name = get_branch_name(self.repo_path.as_str())?;
|
||||
self.last_result = Some((head, name.clone()));
|
||||
Ok(name)
|
||||
}
|
||||
fn fetch(&mut self, head: Head) -> Result<String> {
|
||||
let name = get_branch_name(&self.repo.borrow())?;
|
||||
self.last_result = Some((head, name.clone()));
|
||||
Ok(name)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,105 +1,146 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
sync::{self, CommitId},
|
||||
AsyncGitNotification, StatusItem, CWD,
|
||||
error::Result,
|
||||
sync::{self, commit_files::OldNew, CommitId, RepoPath},
|
||||
AsyncGitNotification, StatusItem,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
|
||||
type ResultType = Vec<StatusItem>;
|
||||
struct Request<R, A>(R, A);
|
||||
|
||||
///
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct CommitFilesParams {
|
||||
///
|
||||
pub id: CommitId,
|
||||
///
|
||||
pub other: Option<CommitId>,
|
||||
}
|
||||
|
||||
impl From<CommitId> for CommitFilesParams {
|
||||
fn from(id: CommitId) -> Self {
|
||||
Self { id, other: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(CommitId, CommitId)> for CommitFilesParams {
|
||||
fn from((id, other): (CommitId, CommitId)) -> Self {
|
||||
Self {
|
||||
id,
|
||||
other: Some(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OldNew<CommitId>> for CommitFilesParams {
|
||||
fn from(old_new: OldNew<CommitId>) -> Self {
|
||||
Self {
|
||||
id: old_new.new,
|
||||
other: Some(old_new.old),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub struct AsyncCommitFiles {
|
||||
current: Arc<Mutex<Option<Request<CommitId, ResultType>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
current:
|
||||
Arc<Mutex<Option<Request<CommitFilesParams, ResultType>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncCommitFiles {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
current: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn current(
|
||||
&mut self,
|
||||
) -> Result<Option<(CommitId, ResultType)>> {
|
||||
let c = self.current.lock()?;
|
||||
///
|
||||
pub fn current(
|
||||
&self,
|
||||
) -> Result<Option<(CommitFilesParams, ResultType)>> {
|
||||
let c = self.current.lock()?;
|
||||
|
||||
c.as_ref()
|
||||
.map_or(Ok(None), |c| Ok(Some((c.0, c.1.clone()))))
|
||||
}
|
||||
c.as_ref()
|
||||
.map_or(Ok(None), |c| Ok(Some((c.0, c.1.clone()))))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fetch(&mut self, id: CommitId) -> Result<()> {
|
||||
if self.is_pending() {
|
||||
return Ok(());
|
||||
}
|
||||
///
|
||||
pub fn fetch(&self, params: CommitFilesParams) -> Result<()> {
|
||||
if self.is_pending() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::trace!("request: {}", id.to_string());
|
||||
log::trace!("request: {params:?}");
|
||||
|
||||
{
|
||||
let current = self.current.lock()?;
|
||||
if let Some(c) = &*current {
|
||||
if c.0 == id {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let current = self.current.lock()?;
|
||||
if let Some(c) = &*current {
|
||||
if c.0 == params {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let repo = self.repo.clone();
|
||||
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
Self::fetch_helper(id, &arc_current)
|
||||
.expect("failed to fetch");
|
||||
rayon_core::spawn(move || {
|
||||
Self::fetch_helper(&repo, params, &arc_current)
|
||||
.expect("failed to fetch");
|
||||
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
|
||||
sender
|
||||
.send(AsyncGitNotification::CommitFiles)
|
||||
.expect("error sending");
|
||||
});
|
||||
sender
|
||||
.send(AsyncGitNotification::CommitFiles)
|
||||
.expect("error sending");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_helper(
|
||||
id: CommitId,
|
||||
arc_current: &Arc<
|
||||
Mutex<Option<Request<CommitId, ResultType>>>,
|
||||
>,
|
||||
) -> Result<()> {
|
||||
let res = sync::get_commit_files(CWD, id)?;
|
||||
fn fetch_helper(
|
||||
repo_path: &RepoPath,
|
||||
params: CommitFilesParams,
|
||||
arc_current: &Arc<
|
||||
Mutex<Option<Request<CommitFilesParams, ResultType>>>,
|
||||
>,
|
||||
) -> Result<()> {
|
||||
let res = sync::get_commit_files(
|
||||
repo_path,
|
||||
params.id,
|
||||
params.other,
|
||||
)?;
|
||||
|
||||
log::trace!(
|
||||
"get_commit_files: {} ({})",
|
||||
id.to_string(),
|
||||
res.len()
|
||||
);
|
||||
log::trace!("get_commit_files: {:?} ({})", params, res.len());
|
||||
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
*current = Some(Request(id, res));
|
||||
}
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
*current = Some(Request(params, res));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,195 +1,221 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{self, CommitId},
|
||||
AsyncGitNotification, FileDiff, CWD,
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{
|
||||
self, commit_files::OldNew, diff::DiffOptions, CommitId,
|
||||
RepoPath,
|
||||
},
|
||||
AsyncGitNotification, FileDiff,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Hash, Clone, PartialEq)]
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub enum DiffType {
|
||||
/// diff in a given commit
|
||||
Commit(CommitId),
|
||||
/// diff against staged file
|
||||
Stage,
|
||||
/// diff against file in workdir
|
||||
WorkDir,
|
||||
/// diff two commits
|
||||
Commits(OldNew<CommitId>),
|
||||
/// diff in a given commit
|
||||
Commit(CommitId),
|
||||
/// diff against staged file
|
||||
Stage,
|
||||
/// diff against file in workdir
|
||||
WorkDir,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Hash, Clone, PartialEq)]
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
pub struct DiffParams {
|
||||
/// path to the file to diff
|
||||
pub path: String,
|
||||
/// what kind of diff
|
||||
pub diff_type: DiffType,
|
||||
/// path to the file to diff
|
||||
pub path: String,
|
||||
/// what kind of diff
|
||||
pub diff_type: DiffType,
|
||||
/// diff options
|
||||
pub options: DiffOptions,
|
||||
}
|
||||
|
||||
struct Request<R, A>(R, Option<A>);
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct LastResult<P, R> {
|
||||
params: P,
|
||||
hash: u64,
|
||||
result: R,
|
||||
params: P,
|
||||
result: R,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct AsyncDiff {
|
||||
current: Arc<Mutex<Request<u64, FileDiff>>>,
|
||||
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
current: Arc<Mutex<Request<u64, FileDiff>>>,
|
||||
last: Arc<Mutex<Option<LastResult<DiffParams, FileDiff>>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncDiff {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last(&mut self) -> Result<Option<(DiffParams, FileDiff)>> {
|
||||
let last = self.last.lock()?;
|
||||
///
|
||||
pub fn last(&self) -> Result<Option<(DiffParams, FileDiff)>> {
|
||||
let last = self.last.lock()?;
|
||||
|
||||
Ok(last.clone().map(|res| (res.params, res.result)))
|
||||
}
|
||||
Ok(last.clone().map(|res| (res.params, res.result)))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
if let Ok(Some(param)) = self.get_last_param() {
|
||||
self.clear_current()?;
|
||||
self.request(param)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
///
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
if let Ok(Some(param)) = self.get_last_param() {
|
||||
self.clear_current()?;
|
||||
self.request(param)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(
|
||||
&mut self,
|
||||
params: DiffParams,
|
||||
) -> Result<Option<FileDiff>> {
|
||||
log::trace!("request");
|
||||
///
|
||||
pub fn request(
|
||||
&self,
|
||||
params: DiffParams,
|
||||
) -> Result<Option<FileDiff>> {
|
||||
log::trace!("request {params:?}");
|
||||
|
||||
let hash = hash(¶ms);
|
||||
let hash = hash(¶ms);
|
||||
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
|
||||
if current.0 == hash {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
if current.0 == hash {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
|
||||
current.0 = hash;
|
||||
current.1 = None;
|
||||
}
|
||||
current.0 = hash;
|
||||
current.1 = None;
|
||||
}
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let repo = self.repo.clone();
|
||||
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
let notify = Self::get_diff_helper(
|
||||
params,
|
||||
&arc_last,
|
||||
&arc_current,
|
||||
hash,
|
||||
);
|
||||
rayon_core::spawn(move || {
|
||||
let notify = Self::get_diff_helper(
|
||||
&repo,
|
||||
params,
|
||||
&arc_last,
|
||||
&arc_current,
|
||||
hash,
|
||||
);
|
||||
|
||||
let notify = match notify {
|
||||
Err(err) => {
|
||||
log::error!("get_diff_helper error: {}", err);
|
||||
true
|
||||
}
|
||||
Ok(notify) => notify,
|
||||
};
|
||||
let notify = match notify {
|
||||
Err(e) => {
|
||||
log::error!("get_diff_helper error: {e}");
|
||||
true
|
||||
}
|
||||
Ok(notify) => notify,
|
||||
};
|
||||
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
|
||||
sender
|
||||
.send(if notify {
|
||||
AsyncGitNotification::Diff
|
||||
} else {
|
||||
AsyncGitNotification::FinishUnchanged
|
||||
})
|
||||
.expect("error sending diff");
|
||||
});
|
||||
sender
|
||||
.send(if notify {
|
||||
AsyncGitNotification::Diff
|
||||
} else {
|
||||
AsyncGitNotification::FinishUnchanged
|
||||
})
|
||||
.expect("error sending diff");
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn get_diff_helper(
|
||||
params: DiffParams,
|
||||
arc_last: &Arc<
|
||||
Mutex<Option<LastResult<DiffParams, FileDiff>>>,
|
||||
>,
|
||||
arc_current: &Arc<Mutex<Request<u64, FileDiff>>>,
|
||||
hash: u64,
|
||||
) -> Result<bool> {
|
||||
let res = match params.diff_type {
|
||||
DiffType::Stage => {
|
||||
sync::diff::get_diff(CWD, ¶ms.path, true)?
|
||||
}
|
||||
DiffType::WorkDir => {
|
||||
sync::diff::get_diff(CWD, ¶ms.path, false)?
|
||||
}
|
||||
DiffType::Commit(id) => sync::diff::get_diff_commit(
|
||||
CWD,
|
||||
id,
|
||||
params.path.clone(),
|
||||
)?,
|
||||
};
|
||||
fn get_diff_helper(
|
||||
repo_path: &RepoPath,
|
||||
params: DiffParams,
|
||||
arc_last: &Arc<
|
||||
Mutex<Option<LastResult<DiffParams, FileDiff>>>,
|
||||
>,
|
||||
arc_current: &Arc<Mutex<Request<u64, FileDiff>>>,
|
||||
hash: u64,
|
||||
) -> Result<bool> {
|
||||
let res = match params.diff_type {
|
||||
DiffType::Stage => sync::diff::get_diff(
|
||||
repo_path,
|
||||
¶ms.path,
|
||||
true,
|
||||
Some(params.options),
|
||||
)?,
|
||||
DiffType::WorkDir => sync::diff::get_diff(
|
||||
repo_path,
|
||||
¶ms.path,
|
||||
false,
|
||||
Some(params.options),
|
||||
)?,
|
||||
DiffType::Commit(id) => sync::diff::get_diff_commit(
|
||||
repo_path,
|
||||
id,
|
||||
params.path.clone(),
|
||||
Some(params.options),
|
||||
)?,
|
||||
DiffType::Commits(ids) => sync::diff::get_diff_commits(
|
||||
repo_path,
|
||||
ids,
|
||||
params.path.clone(),
|
||||
Some(params.options),
|
||||
)?,
|
||||
};
|
||||
|
||||
let mut notify = false;
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash {
|
||||
current.1 = Some(res.clone());
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
let mut notify = false;
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash {
|
||||
current.1 = Some(res.clone());
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = Some(LastResult {
|
||||
result: res,
|
||||
hash,
|
||||
params,
|
||||
});
|
||||
}
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = Some(LastResult {
|
||||
result: res,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(notify)
|
||||
}
|
||||
Ok(notify)
|
||||
}
|
||||
|
||||
fn get_last_param(&self) -> Result<Option<DiffParams>> {
|
||||
Ok(self.last.lock()?.clone().map(|e| e.params))
|
||||
}
|
||||
fn get_last_param(&self) -> Result<Option<DiffParams>> {
|
||||
Ok(self.last.lock()?.clone().map(|e| e.params))
|
||||
}
|
||||
|
||||
fn clear_current(&mut self) -> Result<()> {
|
||||
let mut current = self.current.lock()?;
|
||||
current.0 = 0;
|
||||
current.1 = None;
|
||||
Ok(())
|
||||
}
|
||||
fn clear_current(&self) -> Result<()> {
|
||||
let mut current = self.current.lock()?;
|
||||
current.0 = 0;
|
||||
current.1 = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,60 +1,350 @@
|
|||
#![allow(renamed_and_removed_lints, clippy::unknown_clippy_lints)]
|
||||
|
||||
use std::{num::TryFromIntError, string::FromUtf8Error};
|
||||
use std::{
|
||||
num::TryFromIntError, path::StripPrefixError,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
///
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("`{0}`")]
|
||||
Generic(String),
|
||||
pub enum GixError {
|
||||
///
|
||||
#[error("gix::discover error: {0}")]
|
||||
Discover(#[from] Box<gix::discover::Error>),
|
||||
|
||||
#[error("git: no head found")]
|
||||
NoHead,
|
||||
///
|
||||
#[error("gix::head::peel::to_commit error: {0}")]
|
||||
HeadPeelToCommit(#[from] gix::head::peel::to_commit::Error),
|
||||
|
||||
#[error("git: remote url not found")]
|
||||
UnknownRemote,
|
||||
///
|
||||
#[error("gix::object::find::existing::with_conversion::Error error: {0}")]
|
||||
ObjectFindExistingWithConversion(
|
||||
#[from] gix::object::find::existing::with_conversion::Error,
|
||||
),
|
||||
|
||||
#[error("git: inconclusive remotes")]
|
||||
NoDefaultRemoteFound,
|
||||
///
|
||||
#[error("gix::objs::decode::Error error: {0}")]
|
||||
ObjsDecode(#[from] gix::objs::decode::Error),
|
||||
|
||||
#[error("git: work dir error")]
|
||||
NoWorkDir,
|
||||
///
|
||||
#[error("gix::pathspec::init::Error error: {0}")]
|
||||
PathspecInit(#[from] Box<gix::pathspec::init::Error>),
|
||||
|
||||
#[error("git: uncommitted changes")]
|
||||
UncommittedChanges,
|
||||
///
|
||||
#[error("gix::reference::find::existing error: {0}")]
|
||||
ReferenceFindExisting(
|
||||
#[from] gix::reference::find::existing::Error,
|
||||
),
|
||||
|
||||
#[error("git: can\u{2019}t run blame on a binary file")]
|
||||
NoBlameOnBinaryFile,
|
||||
///
|
||||
#[error("gix::reference::head_tree_id::Error error: {0}")]
|
||||
ReferenceHeadTreeId(#[from] gix::reference::head_tree_id::Error),
|
||||
|
||||
#[error("binary file")]
|
||||
BinaryFile,
|
||||
///
|
||||
#[error("gix::reference::iter::Error error: {0}")]
|
||||
ReferenceIter(#[from] gix::reference::iter::Error),
|
||||
|
||||
#[error("io error:{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
///
|
||||
#[error("gix::reference::iter::init::Error error: {0}")]
|
||||
ReferenceIterInit(#[from] gix::reference::iter::init::Error),
|
||||
|
||||
#[error("git error:{0}")]
|
||||
Git(#[from] git2::Error),
|
||||
///
|
||||
#[error("gix::revision::walk error: {0}")]
|
||||
RevisionWalk(#[from] gix::revision::walk::Error),
|
||||
|
||||
#[error("utf8 error:{0}")]
|
||||
Utf8Conversion(#[from] FromUtf8Error),
|
||||
///
|
||||
#[error("gix::status::Error error: {0}")]
|
||||
Status(#[from] Box<gix::status::Error>),
|
||||
|
||||
#[error("TryFromInt error:{0}")]
|
||||
IntConversion(#[from] TryFromIntError),
|
||||
///
|
||||
#[error("gix::status::index_worktree::Error error: {0}")]
|
||||
StatusIndexWorktree(
|
||||
#[from] Box<gix::status::index_worktree::Error>,
|
||||
),
|
||||
|
||||
#[error("EasyCast error:{0}")]
|
||||
EasyCast(#[from] easy_cast::Error),
|
||||
///
|
||||
#[error("gix::status::into_iter::Error error: {0}")]
|
||||
StatusIntoIter(#[from] Box<gix::status::into_iter::Error>),
|
||||
|
||||
///
|
||||
#[error("gix::status::iter::Error error: {0}")]
|
||||
StatusIter(#[from] Box<gix::status::iter::Error>),
|
||||
|
||||
///
|
||||
#[error("gix::status::tree_index::Error error: {0}")]
|
||||
StatusTreeIndex(#[from] Box<gix::status::tree_index::Error>),
|
||||
|
||||
///
|
||||
#[error("gix::worktree::open_index::Error error: {0}")]
|
||||
WorktreeOpenIndex(#[from] Box<gix::worktree::open_index::Error>),
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
///
|
||||
#[error("`{0}`")]
|
||||
Generic(String),
|
||||
|
||||
///
|
||||
#[error("git: no head found")]
|
||||
NoHead,
|
||||
|
||||
///
|
||||
#[error("git: conflict during rebase")]
|
||||
RebaseConflict,
|
||||
|
||||
///
|
||||
#[error("git: remote url not found")]
|
||||
UnknownRemote,
|
||||
|
||||
///
|
||||
#[error("git: inconclusive remotes")]
|
||||
NoDefaultRemoteFound,
|
||||
|
||||
///
|
||||
#[error("git: work dir error")]
|
||||
NoWorkDir,
|
||||
|
||||
///
|
||||
#[error("git: uncommitted changes")]
|
||||
UncommittedChanges,
|
||||
|
||||
///
|
||||
#[error("git: can\u{2019}t run blame on a binary file")]
|
||||
NoBlameOnBinaryFile,
|
||||
|
||||
///
|
||||
#[error("binary file")]
|
||||
BinaryFile,
|
||||
|
||||
///
|
||||
#[error("io error:{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
///
|
||||
#[error("git error:{0}")]
|
||||
Git(#[from] git2::Error),
|
||||
|
||||
///
|
||||
#[error("git config error: {0}")]
|
||||
GitConfig(String),
|
||||
|
||||
///
|
||||
#[error("strip prefix error: {0}")]
|
||||
StripPrefix(#[from] StripPrefixError),
|
||||
|
||||
///
|
||||
#[error("utf8 error:{0}")]
|
||||
Utf8Conversion(#[from] FromUtf8Error),
|
||||
|
||||
///
|
||||
#[error("TryFromInt error:{0}")]
|
||||
IntConversion(#[from] TryFromIntError),
|
||||
|
||||
///
|
||||
#[error("EasyCast error:{0}")]
|
||||
EasyCast(#[from] easy_cast::Error),
|
||||
|
||||
///
|
||||
#[error("no parent of commit found")]
|
||||
NoParent,
|
||||
|
||||
///
|
||||
#[error("not on a branch")]
|
||||
NoBranch,
|
||||
|
||||
///
|
||||
#[error("rayon error: {0}")]
|
||||
ThreadPool(#[from] rayon_core::ThreadPoolBuildError),
|
||||
|
||||
///
|
||||
#[error("git hook error: {0}")]
|
||||
Hooks(#[from] git2_hooks::HooksError),
|
||||
|
||||
///
|
||||
#[error("sign builder error: {0}")]
|
||||
SignBuilder(#[from] crate::sync::sign::SignBuilderError),
|
||||
|
||||
///
|
||||
#[error("sign error: {0}")]
|
||||
Sign(#[from] crate::sync::sign::SignError),
|
||||
|
||||
///
|
||||
#[error("gix error:{0}")]
|
||||
Gix(#[from] GixError),
|
||||
|
||||
///
|
||||
#[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")]
|
||||
SignAmendNonLastCommit,
|
||||
|
||||
///
|
||||
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording non-last commits")]
|
||||
SignRewordNonLastCommit,
|
||||
|
||||
///
|
||||
#[error("reword error: config commit.gpgsign=true detected.\ngpg signing is not supported for rewording commits with staged changes\ntry unstaging or stashing your changes")]
|
||||
SignRewordLastCommitStaged,
|
||||
}
|
||||
|
||||
///
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||
fn from(error: std::sync::PoisonError<T>) -> Self {
|
||||
Self::Generic(format!("poison error: {}", error))
|
||||
}
|
||||
fn from(error: std::sync::PoisonError<T>) -> Self {
|
||||
Self::Generic(format!("poison error: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<crossbeam_channel::SendError<T>> for Error {
|
||||
fn from(error: crossbeam_channel::SendError<T>) -> Self {
|
||||
Self::Generic(format!("send error: {}", error))
|
||||
}
|
||||
fn from(error: crossbeam_channel::SendError<T>) -> Self {
|
||||
Self::Generic(format!("send error: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::discover::Error> for GixError {
|
||||
fn from(error: gix::discover::Error) -> Self {
|
||||
Self::Discover(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::discover::Error> for Error {
|
||||
fn from(error: gix::discover::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::head::peel::to_commit::Error> for Error {
|
||||
fn from(error: gix::head::peel::to_commit::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::object::find::existing::with_conversion::Error>
|
||||
for Error
|
||||
{
|
||||
fn from(
|
||||
error: gix::object::find::existing::with_conversion::Error,
|
||||
) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::objs::decode::Error> for Error {
|
||||
fn from(error: gix::objs::decode::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::pathspec::init::Error> for GixError {
|
||||
fn from(error: gix::pathspec::init::Error) -> Self {
|
||||
Self::PathspecInit(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::pathspec::init::Error> for Error {
|
||||
fn from(error: gix::pathspec::init::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::reference::find::existing::Error> for Error {
|
||||
fn from(error: gix::reference::find::existing::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::reference::head_tree_id::Error> for Error {
|
||||
fn from(error: gix::reference::head_tree_id::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::reference::iter::Error> for Error {
|
||||
fn from(error: gix::reference::iter::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::reference::iter::init::Error> for Error {
|
||||
fn from(error: gix::reference::iter::init::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::revision::walk::Error> for Error {
|
||||
fn from(error: gix::revision::walk::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::Error> for GixError {
|
||||
fn from(error: gix::status::Error) -> Self {
|
||||
Self::Status(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::Error> for Error {
|
||||
fn from(error: gix::status::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::iter::Error> for GixError {
|
||||
fn from(error: gix::status::iter::Error) -> Self {
|
||||
Self::StatusIter(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::iter::Error> for Error {
|
||||
fn from(error: gix::status::iter::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::into_iter::Error> for GixError {
|
||||
fn from(error: gix::status::into_iter::Error) -> Self {
|
||||
Self::StatusIntoIter(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::into_iter::Error> for Error {
|
||||
fn from(error: gix::status::into_iter::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::index_worktree::Error> for GixError {
|
||||
fn from(error: gix::status::index_worktree::Error) -> Self {
|
||||
Self::StatusIndexWorktree(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::index_worktree::Error> for Error {
|
||||
fn from(error: gix::status::index_worktree::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::tree_index::Error> for GixError {
|
||||
fn from(error: gix::status::tree_index::Error) -> Self {
|
||||
Self::StatusTreeIndex(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::status::tree_index::Error> for Error {
|
||||
fn from(error: gix::status::tree_index::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::worktree::open_index::Error> for GixError {
|
||||
fn from(error: gix::worktree::open_index::Error) -> Self {
|
||||
Self::WorktreeOpenIndex(Box::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::worktree::open_index::Error> for Error {
|
||||
fn from(error: gix::worktree::open_index::Error) -> Self {
|
||||
Self::Gix(GixError::from(error))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,159 +0,0 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::{fetch, push::ProgressNotification},
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress, CWD,
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Sender};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct FetchRequest {
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub branch: String,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct FetchState {
|
||||
request: FetchRequest,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct AsyncFetch {
|
||||
state: Arc<Mutex<Option<FetchState>>>,
|
||||
last_result: Arc<Mutex<Option<(usize, String)>>>,
|
||||
progress: Arc<Mutex<Option<ProgressNotification>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
}
|
||||
|
||||
impl AsyncFetch {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<(usize, String)>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(res.as_ref().map(|progress| progress.clone().into()))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(&mut self, params: FetchRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::Fetch,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
|
||||
let res = fetch(
|
||||
CWD,
|
||||
¶ms.branch,
|
||||
params.basic_credential,
|
||||
Some(progress_sender.clone()),
|
||||
);
|
||||
|
||||
progress_sender
|
||||
.send(ProgressNotification::Done)
|
||||
.expect("closing send failed");
|
||||
|
||||
handle.join().expect("joining thread failed");
|
||||
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
|
||||
sender
|
||||
.send(AsyncGitNotification::Fetch)
|
||||
.expect("AsyncNotification error");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_request(&self, params: &FetchRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
|
||||
*state = Some(FetchState {
|
||||
request: params.clone(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<FetchState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
|
||||
*state = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<(usize, String)>>>,
|
||||
res: Result<usize>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
|
||||
*last_res = match res {
|
||||
Ok(bytes) => Some((bytes, String::new())),
|
||||
Err(e) => {
|
||||
log::error!("fetch error: {}", e);
|
||||
Some((0, e.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
69
asyncgit/src/fetch_job.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
//!
|
||||
|
||||
use crate::{
|
||||
asyncjob::{AsyncJob, RunParams},
|
||||
error::Result,
|
||||
sync::remotes::fetch_all,
|
||||
sync::{cred::BasicAuthCredential, RepoPath},
|
||||
AsyncGitNotification, ProgressPercent,
|
||||
};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
enum JobState {
|
||||
Request(Option<BasicAuthCredential>),
|
||||
Response(Result<()>),
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncFetchJob {
|
||||
state: Arc<Mutex<Option<JobState>>>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
///
|
||||
impl AsyncFetchJob {
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
state: Arc::new(Mutex::new(Some(JobState::Request(
|
||||
basic_credential,
|
||||
)))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncJob for AsyncFetchJob {
|
||||
type Notification = AsyncGitNotification;
|
||||
type Progress = ProgressPercent;
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
_params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
*state = state.take().map(|state| match state {
|
||||
JobState::Request(basic_credentials) => {
|
||||
//TODO: support progress
|
||||
let result = fetch_all(
|
||||
&self.repo,
|
||||
&basic_credentials,
|
||||
&None,
|
||||
);
|
||||
|
||||
JobState::Response(result)
|
||||
}
|
||||
JobState::Response(result) => {
|
||||
JobState::Response(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(AsyncGitNotification::Fetch)
|
||||
}
|
||||
}
|
||||
200
asyncgit/src/filter_commits.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use rayon::{
|
||||
prelude::ParallelIterator,
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
asyncjob::{AsyncJob, RunParams},
|
||||
error::Result,
|
||||
sync::{self, CommitId, RepoPath, SharedCommitFilterFn},
|
||||
AsyncGitNotification, ProgressPercent,
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
///
|
||||
pub struct CommitFilterResult {
|
||||
///
|
||||
pub result: Vec<CommitId>,
|
||||
///
|
||||
pub duration: Duration,
|
||||
}
|
||||
|
||||
enum JobState {
|
||||
Request {
|
||||
commits: Vec<CommitId>,
|
||||
repo_path: RepoPath,
|
||||
},
|
||||
Response(Result<CommitFilterResult>),
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncCommitFilterJob {
|
||||
state: Arc<Mutex<Option<JobState>>>,
|
||||
filter: SharedCommitFilterFn,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
///
|
||||
impl AsyncCommitFilterJob {
|
||||
///
|
||||
pub fn new(
|
||||
repo_path: RepoPath,
|
||||
commits: Vec<CommitId>,
|
||||
filter: SharedCommitFilterFn,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(Some(JobState::Request {
|
||||
repo_path,
|
||||
commits,
|
||||
}))),
|
||||
filter,
|
||||
cancellation_flag,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn result(&self) -> Option<Result<CommitFilterResult>> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
if let Some(state) = state.take() {
|
||||
return match state {
|
||||
JobState::Request { .. } => None,
|
||||
JobState::Response(result) => Some(result),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn run_request(
|
||||
&self,
|
||||
repo_path: &RepoPath,
|
||||
commits: Vec<CommitId>,
|
||||
params: &RunParams<AsyncGitNotification, ProgressPercent>,
|
||||
) -> JobState {
|
||||
let result = self
|
||||
.filter_commits(repo_path, commits, params)
|
||||
.map(|(start, result)| CommitFilterResult {
|
||||
result,
|
||||
duration: start.elapsed(),
|
||||
});
|
||||
|
||||
JobState::Response(result)
|
||||
}
|
||||
|
||||
fn filter_commits(
|
||||
&self,
|
||||
repo_path: &RepoPath,
|
||||
commits: Vec<CommitId>,
|
||||
params: &RunParams<AsyncGitNotification, ProgressPercent>,
|
||||
) -> Result<(Instant, Vec<CommitId>)> {
|
||||
scopetime::scope_time!("filter_commits");
|
||||
|
||||
let total_amount = commits.len();
|
||||
let start = Instant::now();
|
||||
|
||||
//note: for some reason >4 threads degrades search performance
|
||||
let pool =
|
||||
rayon::ThreadPoolBuilder::new().num_threads(4).build()?;
|
||||
|
||||
let idx = AtomicUsize::new(0);
|
||||
|
||||
let mut result = pool.install(|| {
|
||||
commits
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.collect::<Vec<(usize, CommitId)>>()
|
||||
.par_chunks(1000)
|
||||
.filter_map(|c| {
|
||||
//TODO: error log repo open errors
|
||||
sync::repo(repo_path).ok().map(|repo| {
|
||||
c.iter()
|
||||
.filter_map(|(e, c)| {
|
||||
let idx = idx.fetch_add(
|
||||
1,
|
||||
std::sync::atomic::Ordering::Relaxed,
|
||||
);
|
||||
|
||||
if self
|
||||
.cancellation_flag
|
||||
.load(Ordering::Relaxed)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Self::update_progress(
|
||||
params,
|
||||
ProgressPercent::new(
|
||||
idx,
|
||||
total_amount,
|
||||
),
|
||||
);
|
||||
|
||||
(*self.filter)(&repo, c)
|
||||
.ok()
|
||||
.and_then(|res| {
|
||||
res.then_some((*e, *c))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
result.par_sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let result = result.into_iter().map(|c| c.1).collect();
|
||||
|
||||
Ok((start, result))
|
||||
}
|
||||
|
||||
fn update_progress(
|
||||
params: &RunParams<AsyncGitNotification, ProgressPercent>,
|
||||
new_progress: ProgressPercent,
|
||||
) {
|
||||
match params.set_progress(new_progress) {
|
||||
Err(e) => log::error!("progress error: {e}"),
|
||||
Ok(result) if result => {
|
||||
if let Err(e) =
|
||||
params.send(AsyncGitNotification::CommitFilter)
|
||||
{
|
||||
log::error!("send error: {e}");
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncJob for AsyncCommitFilterJob {
|
||||
type Notification = AsyncGitNotification;
|
||||
type Progress = ProgressPercent;
|
||||
|
||||
fn run(
|
||||
&mut self,
|
||||
params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
*state = state.take().map(|state| match state {
|
||||
JobState::Request { commits, repo_path } => {
|
||||
self.run_request(&repo_path, commits, ¶ms)
|
||||
}
|
||||
JobState::Response(result) => {
|
||||
JobState::Response(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(AsyncGitNotification::CommitFilter)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,59 @@
|
|||
//! asyncgit
|
||||
/*!
|
||||
`AsyncGit` is a library that provides non-blocking access to Git
|
||||
operations, enabling `GitUI` to perform potentially slow Git operations
|
||||
in the background while keeping the user interface responsive.
|
||||
|
||||
It also provides synchronous Git operations.
|
||||
|
||||
It wraps libraries like git2 and gix.
|
||||
*/
|
||||
|
||||
#![forbid(missing_docs)]
|
||||
#![deny(
|
||||
unused_imports,
|
||||
unused_must_use,
|
||||
dead_code,
|
||||
unstable_name_collisions,
|
||||
unused_assignments
|
||||
mismatched_lifetime_syntaxes,
|
||||
unused_imports,
|
||||
unused_must_use,
|
||||
dead_code,
|
||||
unstable_name_collisions,
|
||||
unused_assignments,
|
||||
deprecated
|
||||
)]
|
||||
#![deny(unstable_name_collisions)]
|
||||
#![deny(clippy::all, clippy::perf, clippy::nursery, clippy::pedantic)]
|
||||
#![deny(clippy::filetype_is_file)]
|
||||
#![deny(clippy::cargo)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![deny(clippy::panic)]
|
||||
#![deny(clippy::match_like_matches_macro)]
|
||||
#![deny(clippy::needless_update)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
//TODO: get this in someday since expect still leads us to crashes sometimes
|
||||
// #![deny(clippy::expect_used)]
|
||||
#![deny(
|
||||
clippy::filetype_is_file,
|
||||
clippy::cargo,
|
||||
clippy::unwrap_used,
|
||||
clippy::panic,
|
||||
clippy::match_like_matches_macro,
|
||||
clippy::needless_update
|
||||
//TODO: get this in someday since expect still leads us to crashes sometimes
|
||||
// clippy::expect_used
|
||||
)]
|
||||
#![allow(
|
||||
clippy::module_name_repetitions,
|
||||
clippy::must_use_candidate,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::empty_docs,
|
||||
clippy::unnecessary_debug_formatting
|
||||
)]
|
||||
//TODO:
|
||||
#![allow(
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::missing_panics_doc,
|
||||
clippy::multiple_crate_versions
|
||||
)]
|
||||
|
||||
pub mod asyncjob;
|
||||
mod blame;
|
||||
mod branches;
|
||||
pub mod cached;
|
||||
mod commit_files;
|
||||
mod diff;
|
||||
mod error;
|
||||
mod fetch;
|
||||
mod fetch_job;
|
||||
mod filter_commits;
|
||||
mod progress;
|
||||
mod pull;
|
||||
mod push;
|
||||
mod push_tags;
|
||||
pub mod remote_progress;
|
||||
|
|
@ -38,65 +62,92 @@ mod revlog;
|
|||
mod status;
|
||||
pub mod sync;
|
||||
mod tags;
|
||||
mod treefiles;
|
||||
|
||||
pub use crate::{
|
||||
blame::{AsyncBlame, BlameParams},
|
||||
commit_files::AsyncCommitFiles,
|
||||
diff::{AsyncDiff, DiffParams, DiffType},
|
||||
fetch::{AsyncFetch, FetchRequest},
|
||||
push::{AsyncPush, PushRequest},
|
||||
push_tags::{AsyncPushTags, PushTagsRequest},
|
||||
remote_progress::{RemoteProgress, RemoteProgressState},
|
||||
revlog::{AsyncLog, FetchStatus},
|
||||
status::{AsyncStatus, StatusParams},
|
||||
sync::{
|
||||
diff::{DiffLine, DiffLineType, FileDiff},
|
||||
status::{StatusItem, StatusItemType},
|
||||
},
|
||||
tags::AsyncTags,
|
||||
blame::{AsyncBlame, BlameParams},
|
||||
branches::AsyncBranchesJob,
|
||||
commit_files::{AsyncCommitFiles, CommitFilesParams},
|
||||
diff::{AsyncDiff, DiffParams, DiffType},
|
||||
error::{Error, Result},
|
||||
fetch_job::AsyncFetchJob,
|
||||
filter_commits::{AsyncCommitFilterJob, CommitFilterResult},
|
||||
progress::ProgressPercent,
|
||||
pull::{AsyncPull, FetchRequest},
|
||||
push::{AsyncPush, PushRequest},
|
||||
push_tags::{AsyncPushTags, PushTagsRequest},
|
||||
remote_progress::{RemoteProgress, RemoteProgressState},
|
||||
revlog::{AsyncLog, FetchStatus},
|
||||
status::{AsyncStatus, StatusParams},
|
||||
sync::{
|
||||
diff::{DiffLine, DiffLineType, FileDiff},
|
||||
remotes::push::PushType,
|
||||
status::{StatusItem, StatusItemType},
|
||||
},
|
||||
tags::AsyncTags,
|
||||
treefiles::AsyncTreeFilesJob,
|
||||
};
|
||||
pub use git2::message_prettify;
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
/// this type is used to communicate events back through the channel
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AsyncGitNotification {
|
||||
/// this indicates that no new state was fetched but that a async process finished
|
||||
FinishUnchanged,
|
||||
///
|
||||
Status,
|
||||
///
|
||||
Diff,
|
||||
///
|
||||
Log,
|
||||
///
|
||||
CommitFiles,
|
||||
///
|
||||
Tags,
|
||||
///
|
||||
Push,
|
||||
///
|
||||
PushTags,
|
||||
///
|
||||
Fetch,
|
||||
///
|
||||
Blame,
|
||||
///
|
||||
//TODO: this does not belong here
|
||||
SyntaxHighlighting,
|
||||
///
|
||||
//TODO: this does not belong here
|
||||
RemoteTags,
|
||||
/// this indicates that no new state was fetched but that a async process finished
|
||||
FinishUnchanged,
|
||||
///
|
||||
Status,
|
||||
///
|
||||
Diff,
|
||||
///
|
||||
Log,
|
||||
///
|
||||
FileLog,
|
||||
///
|
||||
CommitFiles,
|
||||
///
|
||||
Tags,
|
||||
///
|
||||
Push,
|
||||
///
|
||||
PushTags,
|
||||
///
|
||||
Pull,
|
||||
///
|
||||
Blame,
|
||||
///
|
||||
RemoteTags,
|
||||
///
|
||||
Fetch,
|
||||
///
|
||||
Branches,
|
||||
///
|
||||
TreeFiles,
|
||||
///
|
||||
CommitFilter,
|
||||
}
|
||||
|
||||
/// current working directory `./`
|
||||
pub static CWD: &str = "./";
|
||||
|
||||
/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait
|
||||
pub fn hash<T: Hash + ?Sized>(v: &T) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
v.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
let mut hasher = DefaultHasher::new();
|
||||
v.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
///
|
||||
#[cfg(feature = "trace-libgit")]
|
||||
pub fn register_tracing_logging() -> bool {
|
||||
fn git_trace(level: git2::TraceLevel, msg: &[u8]) {
|
||||
log::info!("[{:?}]: {}", level, String::from_utf8_lossy(msg));
|
||||
}
|
||||
git2::trace_set(git2::TraceLevel::Trace, git_trace).is_ok()
|
||||
}
|
||||
|
||||
///
|
||||
#[cfg(not(feature = "trace-libgit"))]
|
||||
pub fn register_tracing_logging() -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,45 +4,51 @@ use easy_cast::{Conv, ConvFloat};
|
|||
use std::cmp;
|
||||
|
||||
///
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct ProgressPercent {
|
||||
/// percent 0..100
|
||||
pub progress: u8,
|
||||
/// percent 0..100
|
||||
pub progress: u8,
|
||||
}
|
||||
|
||||
impl ProgressPercent {
|
||||
///
|
||||
pub fn new(current: usize, total: usize) -> Self {
|
||||
let total = f64::conv(cmp::max(current, total));
|
||||
let progress = f64::conv(current) / total * 100.0;
|
||||
let progress = u8::conv_nearest(progress);
|
||||
Self { progress }
|
||||
}
|
||||
///
|
||||
pub const fn empty() -> Self {
|
||||
Self { progress: 0 }
|
||||
}
|
||||
///
|
||||
pub const fn full() -> Self {
|
||||
Self { progress: 100 }
|
||||
}
|
||||
///
|
||||
pub fn new(current: usize, total: usize) -> Self {
|
||||
let total = f64::conv(cmp::max(current, total));
|
||||
let progress = f64::conv(current) / total * 100.0;
|
||||
let progress = u8::try_conv_nearest(progress).unwrap_or(100);
|
||||
Self { progress }
|
||||
}
|
||||
///
|
||||
pub const fn empty() -> Self {
|
||||
Self { progress: 0 }
|
||||
}
|
||||
///
|
||||
pub const fn full() -> Self {
|
||||
Self { progress: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_progress_zero_total() {
|
||||
let prog = ProgressPercent::new(1, 0);
|
||||
#[test]
|
||||
fn test_progress_zero_total() {
|
||||
let prog = ProgressPercent::new(1, 0);
|
||||
|
||||
assert_eq!(prog.progress, 100);
|
||||
}
|
||||
assert_eq!(prog.progress, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_progress_rounding() {
|
||||
let prog = ProgressPercent::new(2, 10);
|
||||
#[test]
|
||||
fn test_progress_zero_all() {
|
||||
let prog = ProgressPercent::new(0, 0);
|
||||
assert_eq!(prog.progress, 100);
|
||||
}
|
||||
|
||||
assert_eq!(prog.progress, 20);
|
||||
}
|
||||
#[test]
|
||||
fn test_progress_rounding() {
|
||||
let prog = ProgressPercent::new(2, 10);
|
||||
|
||||
assert_eq!(prog.progress, 20);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
163
asyncgit/src/pull.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::{fetch, push::ProgressNotification},
|
||||
RepoPath,
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress,
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Sender};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct FetchRequest {
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub branch: String,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
}
|
||||
|
||||
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct FetchState {}
|
||||
|
||||
///
|
||||
pub struct AsyncPull {
|
||||
state: Arc<Mutex<Option<FetchState>>>,
|
||||
last_result: Arc<Mutex<Option<(usize, String)>>>,
|
||||
progress: Arc<Mutex<Option<ProgressNotification>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncPull {
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<(usize, String)>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(res.as_ref().map(|progress| progress.clone().into()))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(&self, params: FetchRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
let repo = self.repo.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::Pull,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
|
||||
let res = fetch(
|
||||
&repo,
|
||||
¶ms.branch,
|
||||
params.basic_credential,
|
||||
Some(progress_sender.clone()),
|
||||
);
|
||||
|
||||
progress_sender
|
||||
.send(ProgressNotification::Done)
|
||||
.expect("closing send failed");
|
||||
|
||||
handle.join().expect("joining thread failed");
|
||||
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
|
||||
sender
|
||||
.send(AsyncGitNotification::Pull)
|
||||
.expect("AsyncNotification error");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_request(&self, _params: &FetchRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
|
||||
*state = Some(FetchState {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<FetchState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
|
||||
*state = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<(usize, String)>>>,
|
||||
res: Result<usize>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
|
||||
*last_res = match res {
|
||||
Ok(bytes) => Some((bytes, String::new())),
|
||||
Err(e) => {
|
||||
log::error!("fetch error: {e}");
|
||||
Some((0, e.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,163 +1,174 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential, remotes::push::push,
|
||||
remotes::push::ProgressNotification,
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress, CWD,
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::push::push_raw,
|
||||
remotes::push::{ProgressNotification, PushType},
|
||||
RepoPath,
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress,
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Sender};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct PushRequest {
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub branch: String,
|
||||
///
|
||||
pub force: bool,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub branch: String,
|
||||
///
|
||||
pub push_type: PushType,
|
||||
///
|
||||
pub force: bool,
|
||||
///
|
||||
pub delete: bool,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
}
|
||||
|
||||
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct PushState {
|
||||
request: PushRequest,
|
||||
}
|
||||
struct PushState {}
|
||||
|
||||
///
|
||||
pub struct AsyncPush {
|
||||
state: Arc<Mutex<Option<PushState>>>,
|
||||
last_result: Arc<Mutex<Option<String>>>,
|
||||
progress: Arc<Mutex<Option<ProgressNotification>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
state: Arc<Mutex<Option<PushState>>>,
|
||||
last_result: Arc<Mutex<Option<String>>>,
|
||||
progress: Arc<Mutex<Option<ProgressNotification>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncPush {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<String>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<String>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(res.as_ref().map(|progress| progress.clone().into()))
|
||||
}
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<RemoteProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(res.as_ref().map(|progress| progress.clone().into()))
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(&mut self, params: PushRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
///
|
||||
pub fn request(&self, params: PushRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
let repo = self.repo.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::Push,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::Push,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
|
||||
let res = push(
|
||||
CWD,
|
||||
params.remote.as_str(),
|
||||
params.branch.as_str(),
|
||||
params.force,
|
||||
params.basic_credential.clone(),
|
||||
Some(progress_sender.clone()),
|
||||
);
|
||||
let res = push_raw(
|
||||
&repo,
|
||||
params.remote.as_str(),
|
||||
params.branch.as_str(),
|
||||
params.push_type,
|
||||
params.force,
|
||||
params.delete,
|
||||
params.basic_credential.clone(),
|
||||
Some(progress_sender.clone()),
|
||||
);
|
||||
|
||||
progress_sender
|
||||
.send(ProgressNotification::Done)
|
||||
.expect("closing send failed");
|
||||
progress_sender
|
||||
.send(ProgressNotification::Done)
|
||||
.expect("closing send failed");
|
||||
|
||||
handle.join().expect("joining thread failed");
|
||||
handle.join().expect("joining thread failed");
|
||||
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
|
||||
sender
|
||||
.send(AsyncGitNotification::Push)
|
||||
.expect("error sending push");
|
||||
});
|
||||
sender
|
||||
.send(AsyncGitNotification::Push)
|
||||
.expect("error sending push");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_request(&self, params: &PushRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
fn set_request(&self, _params: &PushRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
|
||||
*state = Some(PushState {
|
||||
request: params.clone(),
|
||||
});
|
||||
*state = Some(PushState {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<PushState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<PushState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
|
||||
*state = None;
|
||||
*state = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<String>>>,
|
||||
res: Result<()>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<String>>>,
|
||||
res: Result<()>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
|
||||
*last_res = match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
log::error!("push error: {}", e);
|
||||
Some(e.to_string())
|
||||
}
|
||||
};
|
||||
*last_res = match res {
|
||||
Ok(()) => None,
|
||||
Err(e) => {
|
||||
log::error!("push error: {e}");
|
||||
Some(e.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,153 +1,157 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::tags::{push_tags, PushTagsProgress},
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress, CWD,
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::tags::{push_tags, PushTagsProgress},
|
||||
RepoPath,
|
||||
},
|
||||
AsyncGitNotification, RemoteProgress,
|
||||
};
|
||||
use crossbeam_channel::{unbounded, Sender};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct PushTagsRequest {
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
///
|
||||
pub remote: String,
|
||||
///
|
||||
pub basic_credential: Option<BasicAuthCredential>,
|
||||
}
|
||||
|
||||
//TODO: since this is empty we can go with a simple AtomicBool to mark that we are fetching or not
|
||||
#[derive(Default, Clone, Debug)]
|
||||
struct PushState {
|
||||
request: PushTagsRequest,
|
||||
}
|
||||
struct PushState {}
|
||||
|
||||
///
|
||||
pub struct AsyncPushTags {
|
||||
state: Arc<Mutex<Option<PushState>>>,
|
||||
last_result: Arc<Mutex<Option<String>>>,
|
||||
progress: Arc<Mutex<Option<PushTagsProgress>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
state: Arc<Mutex<Option<PushState>>>,
|
||||
last_result: Arc<Mutex<Option<String>>>,
|
||||
progress: Arc<Mutex<Option<PushTagsProgress>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
impl AsyncPushTags {
|
||||
///
|
||||
pub fn new(sender: &Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
last_result: Arc::new(Mutex::new(None)),
|
||||
progress: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> Result<bool> {
|
||||
let state = self.state.lock()?;
|
||||
Ok(state.is_some())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<String>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
///
|
||||
pub fn last_result(&self) -> Result<Option<String>> {
|
||||
let res = self.last_result.lock()?;
|
||||
Ok(res.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<PushTagsProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(*res)
|
||||
}
|
||||
///
|
||||
pub fn progress(&self) -> Result<Option<PushTagsProgress>> {
|
||||
let res = self.progress.lock()?;
|
||||
Ok(*res)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn request(&mut self, params: PushTagsRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
///
|
||||
pub fn request(&self, params: PushTagsRequest) -> Result<()> {
|
||||
log::trace!("request");
|
||||
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
if self.is_pending()? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
self.set_request(¶ms)?;
|
||||
RemoteProgress::set_progress(&self.progress, None)?;
|
||||
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
let arc_state = Arc::clone(&self.state);
|
||||
let arc_res = Arc::clone(&self.last_result);
|
||||
let arc_progress = Arc::clone(&self.progress);
|
||||
let sender = self.sender.clone();
|
||||
let repo = self.repo.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
thread::spawn(move || {
|
||||
let (progress_sender, receiver) = unbounded();
|
||||
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::PushTags,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
let handle = RemoteProgress::spawn_receiver_thread(
|
||||
AsyncGitNotification::PushTags,
|
||||
sender.clone(),
|
||||
receiver,
|
||||
arc_progress,
|
||||
);
|
||||
|
||||
let res = push_tags(
|
||||
CWD,
|
||||
params.remote.as_str(),
|
||||
params.basic_credential.clone(),
|
||||
Some(progress_sender),
|
||||
);
|
||||
let res = push_tags(
|
||||
&repo,
|
||||
params.remote.as_str(),
|
||||
params.basic_credential.clone(),
|
||||
Some(progress_sender),
|
||||
);
|
||||
|
||||
handle.join().expect("joining thread failed");
|
||||
handle.join().expect("joining thread failed");
|
||||
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
Self::set_result(&arc_res, res).expect("result error");
|
||||
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
Self::clear_request(&arc_state).expect("clear error");
|
||||
|
||||
sender
|
||||
.send(AsyncGitNotification::PushTags)
|
||||
.expect("error sending push");
|
||||
});
|
||||
sender
|
||||
.send(AsyncGitNotification::PushTags)
|
||||
.expect("error sending push");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_request(&self, params: &PushTagsRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
fn set_request(&self, _params: &PushTagsRequest) -> Result<()> {
|
||||
let mut state = self.state.lock()?;
|
||||
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
if state.is_some() {
|
||||
return Err(Error::Generic("pending request".into()));
|
||||
}
|
||||
|
||||
*state = Some(PushState {
|
||||
request: params.clone(),
|
||||
});
|
||||
*state = Some(PushState {});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<PushState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
fn clear_request(
|
||||
state: &Arc<Mutex<Option<PushState>>>,
|
||||
) -> Result<()> {
|
||||
let mut state = state.lock()?;
|
||||
|
||||
*state = None;
|
||||
*state = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<String>>>,
|
||||
res: Result<()>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
fn set_result(
|
||||
arc_result: &Arc<Mutex<Option<String>>>,
|
||||
res: Result<()>,
|
||||
) -> Result<()> {
|
||||
let mut last_res = arc_result.lock()?;
|
||||
|
||||
*last_res = match res {
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
log::error!("push error: {}", e);
|
||||
Some(e.to_string())
|
||||
}
|
||||
};
|
||||
*last_res = match res {
|
||||
Ok(()) => None,
|
||||
Err(e) => {
|
||||
log::error!("push error: {e}");
|
||||
Some(e.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,151 +1,148 @@
|
|||
//!
|
||||
|
||||
use crate::{
|
||||
error::Result,
|
||||
progress::ProgressPercent,
|
||||
sync::remotes::push::{AsyncProgress, ProgressNotification},
|
||||
AsyncGitNotification,
|
||||
error::Result,
|
||||
progress::ProgressPercent,
|
||||
sync::remotes::push::{AsyncProgress, ProgressNotification},
|
||||
AsyncGitNotification,
|
||||
};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use git2::PackBuilderStage;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
sync::{Arc, Mutex},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
|
||||
/// used for push/pull
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RemoteProgressState {
|
||||
///
|
||||
PackingAddingObject,
|
||||
///
|
||||
PackingDeltafiction,
|
||||
///
|
||||
Pushing,
|
||||
/// fetch progress
|
||||
Transfer,
|
||||
/// remote progress done
|
||||
Done,
|
||||
///
|
||||
PackingAddingObject,
|
||||
///
|
||||
PackingDeltafiction,
|
||||
///
|
||||
Pushing,
|
||||
/// fetch progress
|
||||
Transfer,
|
||||
/// remote progress done
|
||||
Done,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RemoteProgress {
|
||||
///
|
||||
pub state: RemoteProgressState,
|
||||
///
|
||||
pub progress: ProgressPercent,
|
||||
///
|
||||
pub state: RemoteProgressState,
|
||||
///
|
||||
pub progress: ProgressPercent,
|
||||
}
|
||||
|
||||
impl RemoteProgress {
|
||||
///
|
||||
pub fn new(
|
||||
state: RemoteProgressState,
|
||||
current: usize,
|
||||
total: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
state,
|
||||
progress: ProgressPercent::new(current, total),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
state: RemoteProgressState,
|
||||
current: usize,
|
||||
total: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
state,
|
||||
progress: ProgressPercent::new(current, total),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn get_progress_percent(&self) -> u8 {
|
||||
self.progress.progress
|
||||
}
|
||||
///
|
||||
pub const fn get_progress_percent(&self) -> u8 {
|
||||
self.progress.progress
|
||||
}
|
||||
|
||||
pub(crate) fn set_progress<T>(
|
||||
progress: &Arc<Mutex<Option<T>>>,
|
||||
state: Option<T>,
|
||||
) -> Result<()> {
|
||||
let mut progress = progress.lock()?;
|
||||
pub(crate) fn set_progress<T>(
|
||||
progress: &Arc<Mutex<Option<T>>>,
|
||||
state: Option<T>,
|
||||
) -> Result<()> {
|
||||
let mut progress = progress.lock()?;
|
||||
|
||||
*progress = state;
|
||||
*progress = state;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// spawn thread to listen to progress notifcations coming in from blocking remote git method (fetch/push)
|
||||
pub(crate) fn spawn_receiver_thread<
|
||||
T: 'static + AsyncProgress,
|
||||
>(
|
||||
notification_type: AsyncGitNotification,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
receiver: Receiver<T>,
|
||||
progress: Arc<Mutex<Option<T>>>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || loop {
|
||||
let incoming = receiver.recv();
|
||||
match incoming {
|
||||
Ok(update) => {
|
||||
Self::set_progress(
|
||||
&progress,
|
||||
Some(update.clone()),
|
||||
)
|
||||
.expect("set progress failed");
|
||||
sender
|
||||
.send(notification_type)
|
||||
.expect("Notification error");
|
||||
/// spawn thread to listen to progress notifications coming in from blocking remote git method (fetch/push)
|
||||
pub(crate) fn spawn_receiver_thread<
|
||||
T: 'static + AsyncProgress,
|
||||
>(
|
||||
notification_type: AsyncGitNotification,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
receiver: Receiver<T>,
|
||||
progress: Arc<Mutex<Option<T>>>,
|
||||
) -> JoinHandle<()> {
|
||||
thread::spawn(move || loop {
|
||||
let incoming = receiver.recv();
|
||||
match incoming {
|
||||
Ok(update) => {
|
||||
Self::set_progress(
|
||||
&progress,
|
||||
Some(update.clone()),
|
||||
)
|
||||
.expect("set progress failed");
|
||||
sender
|
||||
.send(notification_type)
|
||||
.expect("Notification error");
|
||||
|
||||
//NOTE: for better debugging
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
thread::yield_now();
|
||||
|
||||
if update.is_done() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"remote progress receiver error: {}",
|
||||
e
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if update.is_done() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"remote progress receiver error: {e}",
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgressNotification> for RemoteProgress {
|
||||
fn from(progress: ProgressNotification) -> Self {
|
||||
match progress {
|
||||
ProgressNotification::Packing {
|
||||
stage,
|
||||
current,
|
||||
total,
|
||||
} => match stage {
|
||||
PackBuilderStage::AddingObjects => Self::new(
|
||||
RemoteProgressState::PackingAddingObject,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
PackBuilderStage::Deltafication => Self::new(
|
||||
RemoteProgressState::PackingDeltafiction,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
},
|
||||
ProgressNotification::PushTransfer {
|
||||
current,
|
||||
total,
|
||||
..
|
||||
} => Self::new(
|
||||
RemoteProgressState::Pushing,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
ProgressNotification::Transfer {
|
||||
objects,
|
||||
total_objects,
|
||||
..
|
||||
} => Self::new(
|
||||
RemoteProgressState::Transfer,
|
||||
objects,
|
||||
total_objects,
|
||||
),
|
||||
_ => Self::new(RemoteProgressState::Done, 1, 1),
|
||||
}
|
||||
}
|
||||
fn from(progress: ProgressNotification) -> Self {
|
||||
match progress {
|
||||
ProgressNotification::Packing {
|
||||
stage,
|
||||
current,
|
||||
total,
|
||||
} => match stage {
|
||||
PackBuilderStage::AddingObjects => Self::new(
|
||||
RemoteProgressState::PackingAddingObject,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
PackBuilderStage::Deltafication => Self::new(
|
||||
RemoteProgressState::PackingDeltafiction,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
},
|
||||
ProgressNotification::PushTransfer {
|
||||
current,
|
||||
total,
|
||||
..
|
||||
} => Self::new(
|
||||
RemoteProgressState::Pushing,
|
||||
current,
|
||||
total,
|
||||
),
|
||||
ProgressNotification::Transfer {
|
||||
objects,
|
||||
total_objects,
|
||||
..
|
||||
} => Self::new(
|
||||
RemoteProgressState::Transfer,
|
||||
objects,
|
||||
total_objects,
|
||||
),
|
||||
_ => Self::new(RemoteProgressState::Done, 1, 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,88 @@
|
|||
//!
|
||||
|
||||
use crate::{
|
||||
asyncjob::AsyncJob,
|
||||
error::Result,
|
||||
sync::cred::BasicAuthCredential,
|
||||
sync::remotes::{get_default_remote, tags_missing_remote},
|
||||
CWD,
|
||||
asyncjob::{AsyncJob, RunParams},
|
||||
error::Result,
|
||||
sync::cred::BasicAuthCredential,
|
||||
sync::{
|
||||
remotes::{get_default_remote, tags_missing_remote},
|
||||
RepoPath,
|
||||
},
|
||||
AsyncGitNotification,
|
||||
};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
enum JobState {
|
||||
Request(Option<BasicAuthCredential>),
|
||||
Response(Result<Vec<String>>),
|
||||
Request(Option<BasicAuthCredential>),
|
||||
Response(Result<Vec<String>>),
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncRemoteTagsJob {
|
||||
state: Arc<Mutex<Option<JobState>>>,
|
||||
state: Arc<Mutex<Option<JobState>>>,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
///
|
||||
impl AsyncRemoteTagsJob {
|
||||
///
|
||||
pub fn new(
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: Arc::new(Mutex::new(Some(JobState::Request(
|
||||
basic_credential,
|
||||
)))),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
state: Arc::new(Mutex::new(Some(JobState::Request(
|
||||
basic_credential,
|
||||
)))),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn result(&self) -> Option<Result<Vec<String>>> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
if let Some(state) = state.take() {
|
||||
return match state {
|
||||
JobState::Request(_) => None,
|
||||
JobState::Response(result) => Some(result),
|
||||
};
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn result(&self) -> Option<Result<Vec<String>>> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
if let Some(state) = state.take() {
|
||||
return match state {
|
||||
JobState::Request(_) => None,
|
||||
JobState::Response(result) => Some(result),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncJob for AsyncRemoteTagsJob {
|
||||
fn run(&mut self) {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
*state = state.take().map(|state| match state {
|
||||
JobState::Request(basic_credential) => {
|
||||
let result =
|
||||
get_default_remote(CWD).and_then(|remote| {
|
||||
tags_missing_remote(
|
||||
CWD,
|
||||
&remote,
|
||||
basic_credential,
|
||||
)
|
||||
});
|
||||
type Notification = AsyncGitNotification;
|
||||
type Progress = ();
|
||||
|
||||
JobState::Response(result)
|
||||
}
|
||||
JobState::Response(result) => {
|
||||
JobState::Response(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fn run(
|
||||
&mut self,
|
||||
_params: RunParams<Self::Notification, Self::Progress>,
|
||||
) -> Result<Self::Notification> {
|
||||
if let Ok(mut state) = self.state.lock() {
|
||||
*state = state.take().map(|state| match state {
|
||||
JobState::Request(basic_credential) => {
|
||||
let result = get_default_remote(&self.repo)
|
||||
.and_then(|remote| {
|
||||
tags_missing_remote(
|
||||
&self.repo,
|
||||
&remote,
|
||||
basic_credential,
|
||||
)
|
||||
});
|
||||
|
||||
JobState::Response(result)
|
||||
}
|
||||
JobState::Response(result) => {
|
||||
JobState::Response(result)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(AsyncGitNotification::RemoteTags)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,201 +1,405 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
sync::{utils::repo, CommitId, LogWalker, LogWalkerFilter},
|
||||
AsyncGitNotification, CWD,
|
||||
error::Result,
|
||||
sync::{
|
||||
gix_repo, repo, CommitId, LogWalker, LogWalkerWithoutFilter,
|
||||
RepoPath, SharedCommitFilterFn,
|
||||
},
|
||||
AsyncGitNotification, Error,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::Oid;
|
||||
use scopetime::scope_time;
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum FetchStatus {
|
||||
/// previous fetch still running
|
||||
Pending,
|
||||
/// no change expected
|
||||
NoChange,
|
||||
/// new walk was started
|
||||
Started,
|
||||
/// previous fetch still running
|
||||
Pending,
|
||||
/// no change expected
|
||||
NoChange,
|
||||
/// new walk was started
|
||||
Started,
|
||||
}
|
||||
|
||||
///
|
||||
pub struct AsyncLogResult {
|
||||
///
|
||||
pub commits: Vec<CommitId>,
|
||||
///
|
||||
pub duration: Duration,
|
||||
}
|
||||
///
|
||||
pub struct AsyncLog {
|
||||
current: Arc<Mutex<Vec<CommitId>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicBool>,
|
||||
background: Arc<AtomicBool>,
|
||||
filter: Option<LogWalkerFilter>,
|
||||
current: Arc<Mutex<AsyncLogResult>>,
|
||||
current_head: Arc<Mutex<Option<CommitId>>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicBool>,
|
||||
background: Arc<AtomicBool>,
|
||||
filter: Option<SharedCommitFilterFn>,
|
||||
partial_extract: AtomicBool,
|
||||
repo: RepoPath,
|
||||
}
|
||||
|
||||
static LIMIT_COUNT: usize = 3000;
|
||||
static SLEEP_FOREGROUND: Duration = Duration::from_millis(2);
|
||||
static SLEEP_BACKGROUND: Duration = Duration::from_millis(1000);
|
||||
static SLEEP_BACKGROUND: Duration = Duration::from_secs(1);
|
||||
|
||||
impl AsyncLog {
|
||||
///
|
||||
pub fn new(
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
filter: Option<LogWalkerFilter>,
|
||||
) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Vec::new())),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicBool::new(false)),
|
||||
background: Arc::new(AtomicBool::new(false)),
|
||||
filter,
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
filter: Option<SharedCommitFilterFn>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
current: Arc::new(Mutex::new(AsyncLogResult {
|
||||
commits: Vec::new(),
|
||||
duration: Duration::default(),
|
||||
})),
|
||||
current_head: Arc::new(Mutex::new(None)),
|
||||
sender: sender.clone(),
|
||||
pending: Arc::new(AtomicBool::new(false)),
|
||||
background: Arc::new(AtomicBool::new(false)),
|
||||
filter,
|
||||
partial_extract: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn count(&mut self) -> Result<usize> {
|
||||
Ok(self.current.lock()?.len())
|
||||
}
|
||||
///
|
||||
pub fn count(&self) -> Result<usize> {
|
||||
Ok(self.current.lock()?.commits.len())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_slice(
|
||||
&self,
|
||||
start_index: usize,
|
||||
amount: usize,
|
||||
) -> Result<Vec<CommitId>> {
|
||||
let list = self.current.lock()?;
|
||||
let list_len = list.len();
|
||||
let min = start_index.min(list_len);
|
||||
let max = min + amount;
|
||||
let max = max.min(list_len);
|
||||
Ok(list[min..max].to_vec())
|
||||
}
|
||||
///
|
||||
pub fn get_slice(
|
||||
&self,
|
||||
start_index: usize,
|
||||
amount: usize,
|
||||
) -> Result<Vec<CommitId>> {
|
||||
if self.partial_extract.load(Ordering::Relaxed) {
|
||||
return Err(Error::Generic(String::from("Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!")));
|
||||
}
|
||||
|
||||
///
|
||||
pub fn position(&self, id: CommitId) -> Result<Option<usize>> {
|
||||
let list = self.current.lock()?;
|
||||
let position = list.iter().position(|&x| x == id);
|
||||
let list = &self.current.lock()?.commits;
|
||||
let list_len = list.len();
|
||||
let min = start_index.min(list_len);
|
||||
let max = min + amount;
|
||||
let max = max.min(list_len);
|
||||
Ok(list[min..max].to_vec())
|
||||
}
|
||||
|
||||
Ok(position)
|
||||
}
|
||||
///
|
||||
pub fn get_items(&self) -> Result<Vec<CommitId>> {
|
||||
if self.partial_extract.load(Ordering::Relaxed) {
|
||||
return Err(Error::Generic(String::from("Faulty usage of AsyncLog: Cannot partially extract items and rely on get_items slice to still work!")));
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed)
|
||||
}
|
||||
let list = &self.current.lock()?.commits;
|
||||
Ok(list.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn set_background(&mut self) {
|
||||
self.background.store(true, Ordering::Relaxed);
|
||||
}
|
||||
///
|
||||
pub fn extract_items(&self) -> Result<Vec<CommitId>> {
|
||||
self.partial_extract.store(true, Ordering::Relaxed);
|
||||
let list = &mut self.current.lock()?.commits;
|
||||
let result = list.clone();
|
||||
list.clear();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
///
|
||||
fn current_head(&self) -> Result<CommitId> {
|
||||
Ok(self
|
||||
.current
|
||||
.lock()?
|
||||
.first()
|
||||
.map_or(Oid::zero().into(), |f| *f))
|
||||
}
|
||||
///
|
||||
pub fn get_last_duration(&self) -> Result<Duration> {
|
||||
Ok(self.current.lock()?.duration)
|
||||
}
|
||||
|
||||
///
|
||||
fn head_changed(&self) -> Result<bool> {
|
||||
if let Ok(head) = repo(CWD)?.head() {
|
||||
if let Some(head) = head.target() {
|
||||
return Ok(head != self.current_head()?.into());
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fetch(&mut self) -> Result<FetchStatus> {
|
||||
self.background.store(false, Ordering::Relaxed);
|
||||
///
|
||||
pub fn set_background(&self) {
|
||||
self.background.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
if self.is_pending() {
|
||||
return Ok(FetchStatus::Pending);
|
||||
}
|
||||
///
|
||||
fn current_head(&self) -> Result<Option<CommitId>> {
|
||||
Ok(*self.current_head.lock()?)
|
||||
}
|
||||
|
||||
if !self.head_changed()? {
|
||||
return Ok(FetchStatus::NoChange);
|
||||
}
|
||||
///
|
||||
fn head_changed(&self) -> Result<bool> {
|
||||
if let Ok(head) = repo(&self.repo)?.head() {
|
||||
return Ok(
|
||||
head.target() != self.current_head()?.map(Into::into)
|
||||
);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
///
|
||||
pub fn fetch(&self) -> Result<FetchStatus> {
|
||||
self.background.store(false, Ordering::Relaxed);
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let arc_background = Arc::clone(&self.background);
|
||||
if self.is_pending() {
|
||||
return Ok(FetchStatus::Pending);
|
||||
}
|
||||
|
||||
self.pending.store(true, Ordering::Relaxed);
|
||||
if !self.head_changed()? {
|
||||
return Ok(FetchStatus::NoChange);
|
||||
}
|
||||
|
||||
let filter = self.filter.clone();
|
||||
self.pending.store(true, Ordering::Relaxed);
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
scope_time!("async::revlog");
|
||||
self.clear()?;
|
||||
|
||||
Self::fetch_helper(
|
||||
&arc_current,
|
||||
&arc_background,
|
||||
&sender,
|
||||
filter,
|
||||
)
|
||||
.expect("failed to fetch");
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let arc_background = Arc::clone(&self.background);
|
||||
let filter = self.filter.clone();
|
||||
let repo_path = self.repo.clone();
|
||||
|
||||
arc_pending.store(false, Ordering::Relaxed);
|
||||
if let Ok(head) = repo(&self.repo)?.head() {
|
||||
*self.current_head.lock()? =
|
||||
head.target().map(CommitId::new);
|
||||
}
|
||||
|
||||
Self::notify(&sender);
|
||||
});
|
||||
rayon_core::spawn(move || {
|
||||
scope_time!("async::revlog");
|
||||
|
||||
Ok(FetchStatus::Started)
|
||||
}
|
||||
Self::fetch_helper(
|
||||
&repo_path,
|
||||
&arc_current,
|
||||
&arc_background,
|
||||
&sender,
|
||||
filter,
|
||||
)
|
||||
.expect("failed to fetch");
|
||||
|
||||
fn fetch_helper(
|
||||
arc_current: &Arc<Mutex<Vec<CommitId>>>,
|
||||
arc_background: &Arc<AtomicBool>,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
filter: Option<LogWalkerFilter>,
|
||||
) -> Result<()> {
|
||||
let mut entries = Vec::with_capacity(LIMIT_COUNT);
|
||||
let r = repo(CWD)?;
|
||||
let mut walker =
|
||||
LogWalker::new(&r, LIMIT_COUNT)?.filter(filter);
|
||||
loop {
|
||||
entries.clear();
|
||||
let res_is_err = walker.read(&mut entries).is_err();
|
||||
arc_pending.store(false, Ordering::Relaxed);
|
||||
|
||||
if !res_is_err {
|
||||
let mut current = arc_current.lock()?;
|
||||
current.extend(entries.iter());
|
||||
}
|
||||
Self::notify(&sender);
|
||||
});
|
||||
|
||||
if res_is_err || entries.len() <= 1 {
|
||||
break;
|
||||
}
|
||||
Self::notify(sender);
|
||||
Ok(FetchStatus::Started)
|
||||
}
|
||||
|
||||
let sleep_duration =
|
||||
if arc_background.load(Ordering::Relaxed) {
|
||||
SLEEP_BACKGROUND
|
||||
} else {
|
||||
SLEEP_FOREGROUND
|
||||
};
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
fn fetch_helper(
|
||||
repo_path: &RepoPath,
|
||||
arc_current: &Arc<Mutex<AsyncLogResult>>,
|
||||
arc_background: &Arc<AtomicBool>,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
filter: Option<SharedCommitFilterFn>,
|
||||
) -> Result<()> {
|
||||
filter.map_or_else(
|
||||
|| {
|
||||
Self::fetch_helper_without_filter(
|
||||
repo_path,
|
||||
arc_current,
|
||||
arc_background,
|
||||
sender,
|
||||
)
|
||||
},
|
||||
|filter| {
|
||||
Self::fetch_helper_with_filter(
|
||||
repo_path,
|
||||
arc_current,
|
||||
arc_background,
|
||||
sender,
|
||||
filter,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn fetch_helper_with_filter(
|
||||
repo_path: &RepoPath,
|
||||
arc_current: &Arc<Mutex<AsyncLogResult>>,
|
||||
arc_background: &Arc<AtomicBool>,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
filter: SharedCommitFilterFn,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
fn clear(&mut self) -> Result<()> {
|
||||
self.current.lock()?.clear();
|
||||
Ok(())
|
||||
}
|
||||
let mut entries = vec![CommitId::default(); LIMIT_COUNT];
|
||||
entries.resize(0, CommitId::default());
|
||||
|
||||
fn notify(sender: &Sender<AsyncGitNotification>) {
|
||||
sender
|
||||
.send(AsyncGitNotification::Log)
|
||||
.expect("error sending");
|
||||
}
|
||||
let r = repo(repo_path)?;
|
||||
let mut walker =
|
||||
LogWalker::new(&r, LIMIT_COUNT)?.filter(Some(filter));
|
||||
|
||||
loop {
|
||||
entries.clear();
|
||||
let read = walker.read(&mut entries)?;
|
||||
|
||||
let mut current = arc_current.lock()?;
|
||||
current.commits.extend(entries.iter());
|
||||
current.duration = start_time.elapsed();
|
||||
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
Self::notify(sender);
|
||||
|
||||
let sleep_duration =
|
||||
if arc_background.load(Ordering::Relaxed) {
|
||||
SLEEP_BACKGROUND
|
||||
} else {
|
||||
SLEEP_FOREGROUND
|
||||
};
|
||||
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
|
||||
log::trace!("revlog visited: {}", walker.visited());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_helper_without_filter(
|
||||
repo_path: &RepoPath,
|
||||
arc_current: &Arc<Mutex<AsyncLogResult>>,
|
||||
arc_background: &Arc<AtomicBool>,
|
||||
sender: &Sender<AsyncGitNotification>,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let mut entries = vec![CommitId::default(); LIMIT_COUNT];
|
||||
entries.resize(0, CommitId::default());
|
||||
|
||||
let mut repo: gix::Repository = gix_repo(repo_path)?;
|
||||
let mut walker =
|
||||
LogWalkerWithoutFilter::new(&mut repo, LIMIT_COUNT)?;
|
||||
|
||||
loop {
|
||||
entries.clear();
|
||||
let read = walker.read(&mut entries)?;
|
||||
|
||||
let mut current = arc_current.lock()?;
|
||||
current.commits.extend(entries.iter());
|
||||
current.duration = start_time.elapsed();
|
||||
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
Self::notify(sender);
|
||||
|
||||
let sleep_duration =
|
||||
if arc_background.load(Ordering::Relaxed) {
|
||||
SLEEP_BACKGROUND
|
||||
} else {
|
||||
SLEEP_FOREGROUND
|
||||
};
|
||||
|
||||
thread::sleep(sleep_duration);
|
||||
}
|
||||
|
||||
log::trace!("revlog visited: {}", walker.visited());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&self) -> Result<()> {
|
||||
self.current.lock()?.commits.clear();
|
||||
*self.current_head.lock()? = None;
|
||||
self.partial_extract.store(false, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify(sender: &Sender<AsyncGitNotification>) {
|
||||
sender
|
||||
.send(AsyncGitNotification::Log)
|
||||
.expect("error sending");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam_channel::unbounded;
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::sync::tests::{debug_cmd_print, repo_init};
|
||||
use crate::sync::RepoPath;
|
||||
use crate::AsyncLog;
|
||||
|
||||
use super::AsyncLogResult;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_smoke_in_subdir() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: RepoPath =
|
||||
root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let (tx_git, _rx_git) = unbounded();
|
||||
|
||||
debug_cmd_print(&repo_path, "mkdir subdir");
|
||||
|
||||
let subdir = repo.path().parent().unwrap().join("subdir");
|
||||
let subdir_path: RepoPath =
|
||||
subdir.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let arc_current = Arc::new(Mutex::new(AsyncLogResult {
|
||||
commits: Vec::new(),
|
||||
duration: Duration::default(),
|
||||
}));
|
||||
let arc_background = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let result = AsyncLog::fetch_helper_without_filter(
|
||||
&subdir_path,
|
||||
&arc_current,
|
||||
&arc_background,
|
||||
&tx_git,
|
||||
);
|
||||
|
||||
assert_eq!(result.unwrap(), ());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_env_variables() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let git_dir = repo.path();
|
||||
|
||||
let (tx_git, _rx_git) = unbounded();
|
||||
|
||||
let empty_dir = TempDir::new().unwrap();
|
||||
let empty_path: RepoPath =
|
||||
empty_dir.path().to_str().unwrap().into();
|
||||
|
||||
let arc_current = Arc::new(Mutex::new(AsyncLogResult {
|
||||
commits: Vec::new(),
|
||||
duration: Duration::default(),
|
||||
}));
|
||||
let arc_background = Arc::new(AtomicBool::new(false));
|
||||
|
||||
std::env::set_var("GIT_DIR", git_dir);
|
||||
|
||||
let result = AsyncLog::fetch_helper_without_filter(
|
||||
// We pass an empty path, thus testing whether `GIT_DIR`, set above, is taken into account.
|
||||
&empty_path,
|
||||
&arc_current,
|
||||
&arc_background,
|
||||
&tx_git,
|
||||
);
|
||||
|
||||
std::env::remove_var("GIT_DIR");
|
||||
|
||||
assert_eq!(result.unwrap(), ());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,169 +1,191 @@
|
|||
use crate::{
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{self, status::StatusType},
|
||||
AsyncGitNotification, StatusItem, CWD,
|
||||
error::Result,
|
||||
hash,
|
||||
sync::{
|
||||
self, status::StatusType, RepoPath, ShowUntrackedFilesConfig,
|
||||
},
|
||||
AsyncGitNotification, StatusItem,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
hash::Hash,
|
||||
sync::{
|
||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
fn current_tick() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("time before unix epoch!")
|
||||
.as_millis()
|
||||
}
|
||||
|
||||
#[derive(Default, Hash, Clone)]
|
||||
pub struct Status {
|
||||
pub items: Vec<StatusItem>,
|
||||
pub items: Vec<StatusItem>,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default, Hash, Copy, Clone, PartialEq)]
|
||||
#[derive(Default, Hash, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct StatusParams {
|
||||
tick: u128,
|
||||
status_type: StatusType,
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
}
|
||||
|
||||
impl StatusParams {
|
||||
///
|
||||
pub fn new(status_type: StatusType) -> Self {
|
||||
Self {
|
||||
tick: current_tick(),
|
||||
status_type,
|
||||
}
|
||||
}
|
||||
///
|
||||
pub const fn new(
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
) -> Self {
|
||||
Self {
|
||||
status_type,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Request<R, A>(R, Option<A>);
|
||||
|
||||
///
|
||||
pub struct AsyncStatus {
|
||||
current: Arc<Mutex<Request<u64, Status>>>,
|
||||
last: Arc<Mutex<Status>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
current: Arc<Mutex<Request<u64, Status>>>,
|
||||
last: Arc<Mutex<Status>>,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
pending: Arc<AtomicUsize>,
|
||||
repo: RepoPath,
|
||||
/// Counter that increments after each completed fetch.
|
||||
generation: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl AsyncStatus {
|
||||
///
|
||||
pub fn new(sender: Sender<AsyncGitNotification>) -> Self {
|
||||
Self {
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(Status::default())),
|
||||
sender,
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn new(
|
||||
repo: RepoPath,
|
||||
sender: Sender<AsyncGitNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
current: Arc::new(Mutex::new(Request(0, None))),
|
||||
last: Arc::new(Mutex::new(Status::default())),
|
||||
sender,
|
||||
pending: Arc::new(AtomicUsize::new(0)),
|
||||
generation: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn last(&mut self) -> Result<Status> {
|
||||
let last = self.last.lock()?;
|
||||
Ok(last.clone())
|
||||
}
|
||||
///
|
||||
pub fn last(&self) -> Result<Status> {
|
||||
let last = self.last.lock()?;
|
||||
Ok(last.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
///
|
||||
pub fn is_pending(&self) -> bool {
|
||||
self.pending.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
///
|
||||
pub fn fetch(
|
||||
&mut self,
|
||||
params: &StatusParams,
|
||||
) -> Result<Option<Status>> {
|
||||
if self.is_pending() {
|
||||
log::trace!("request blocked, still pending");
|
||||
return Ok(None);
|
||||
}
|
||||
///
|
||||
pub fn fetch(
|
||||
&self,
|
||||
params: &StatusParams,
|
||||
) -> Result<Option<Status>> {
|
||||
if self.is_pending() {
|
||||
log::trace!("request blocked, still pending");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let hash_request = hash(¶ms);
|
||||
let generation = self.generation.load(Ordering::Relaxed);
|
||||
let hash_request = hash(&(params, generation));
|
||||
|
||||
log::trace!(
|
||||
"request: [hash: {}] (type: {:?})",
|
||||
hash_request,
|
||||
params.status_type,
|
||||
);
|
||||
log::trace!(
|
||||
"request: [hash: {}] (type: {:?}, gen: {})",
|
||||
hash_request,
|
||||
params.status_type,
|
||||
generation,
|
||||
);
|
||||
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
{
|
||||
let mut current = self.current.lock()?;
|
||||
|
||||
if current.0 == hash_request {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
if current.0 == hash_request {
|
||||
return Ok(current.1.clone());
|
||||
}
|
||||
|
||||
current.0 = hash_request;
|
||||
current.1 = None;
|
||||
}
|
||||
current.0 = hash_request;
|
||||
current.1 = None;
|
||||
}
|
||||
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let status_type = params.status_type;
|
||||
let arc_current = Arc::clone(&self.current);
|
||||
let arc_last = Arc::clone(&self.last);
|
||||
let arc_generation = Arc::clone(&self.generation);
|
||||
let sender = self.sender.clone();
|
||||
let arc_pending = Arc::clone(&self.pending);
|
||||
let status_type = params.status_type;
|
||||
let config = params.config;
|
||||
let repo = self.repo.clone();
|
||||
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
self.pending.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
rayon_core::spawn(move || {
|
||||
let ok = Self::fetch_helper(
|
||||
status_type,
|
||||
hash_request,
|
||||
&arc_current,
|
||||
&arc_last,
|
||||
)
|
||||
.is_ok();
|
||||
rayon_core::spawn(move || {
|
||||
if let Err(e) = Self::fetch_helper(
|
||||
&repo,
|
||||
status_type,
|
||||
config,
|
||||
hash_request,
|
||||
&arc_current,
|
||||
&arc_last,
|
||||
) {
|
||||
log::error!("fetch_helper: {e}");
|
||||
}
|
||||
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
// Increment generation to invalidate cache for next request
|
||||
arc_generation.fetch_add(1, Ordering::Relaxed);
|
||||
arc_pending.fetch_sub(1, Ordering::Relaxed);
|
||||
|
||||
if ok {
|
||||
sender
|
||||
.send(AsyncGitNotification::Status)
|
||||
.expect("error sending status");
|
||||
}
|
||||
});
|
||||
if let Err(e) = sender.send(AsyncGitNotification::Status)
|
||||
{
|
||||
log::error!("send status error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn fetch_helper(
|
||||
status_type: StatusType,
|
||||
hash_request: u64,
|
||||
arc_current: &Arc<Mutex<Request<u64, Status>>>,
|
||||
arc_last: &Arc<Mutex<Status>>,
|
||||
) -> Result<()> {
|
||||
let res = Self::get_status(status_type)?;
|
||||
log::trace!(
|
||||
"status fetched: {} (type: {:?})",
|
||||
hash_request,
|
||||
status_type,
|
||||
);
|
||||
fn fetch_helper(
|
||||
repo: &RepoPath,
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
hash_request: u64,
|
||||
arc_current: &Arc<Mutex<Request<u64, Status>>>,
|
||||
arc_last: &Arc<Mutex<Status>>,
|
||||
) -> Result<()> {
|
||||
let res = Self::get_status(repo, status_type, config)?;
|
||||
log::trace!(
|
||||
"status fetched: {hash_request} (type: {status_type:?})",
|
||||
);
|
||||
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash_request {
|
||||
current.1 = Some(res.clone());
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut current = arc_current.lock()?;
|
||||
if current.0 == hash_request {
|
||||
current.1 = Some(res.clone());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = res;
|
||||
}
|
||||
{
|
||||
let mut last = arc_last.lock()?;
|
||||
*last = res;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_status(status_type: StatusType) -> Result<Status> {
|
||||
Ok(Status {
|
||||
items: sync::status::get_status(CWD, status_type)?,
|
||||
})
|
||||
}
|
||||
fn get_status(
|
||||
repo: &RepoPath,
|
||||
status_type: StatusType,
|
||||
config: Option<ShowUntrackedFilesConfig>,
|
||||
) -> Result<Status> {
|
||||
Ok(Status {
|
||||
items: sync::status::get_status(
|
||||
repo,
|
||||
status_type,
|
||||
config,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
//! Sync git API for fetching a file blame
|
||||
|
||||
use super::{utils, CommitId};
|
||||
use super::{utils, CommitId, RepoPath};
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::get_commits_info,
|
||||
error::{Error, Result},
|
||||
sync::{get_commits_info, repository::repo},
|
||||
};
|
||||
use git2::BlameOptions;
|
||||
use scopetime::scope_time;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
|
@ -13,205 +14,249 @@ use std::path::Path;
|
|||
/// A `BlameHunk` contains all the information that will be shown to the user.
|
||||
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
|
||||
pub struct BlameHunk {
|
||||
///
|
||||
pub commit_id: CommitId,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub time: i64,
|
||||
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
|
||||
/// `start_line` is 0-based because the `Vec` storing the lines starts at
|
||||
/// index 0.
|
||||
pub start_line: usize,
|
||||
///
|
||||
pub end_line: usize,
|
||||
///
|
||||
pub commit_id: CommitId,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub time: i64,
|
||||
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
|
||||
/// `start_line` is 0-based because the `Vec` storing the lines starts at
|
||||
/// index 0.
|
||||
pub start_line: usize,
|
||||
///
|
||||
pub end_line: usize,
|
||||
}
|
||||
|
||||
/// A `BlameFile` represents a collection of lines. This is targeted at how the
|
||||
/// data will be used by the UI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileBlame {
|
||||
///
|
||||
pub commit_id: CommitId,
|
||||
///
|
||||
pub path: String,
|
||||
///
|
||||
pub lines: Vec<(Option<BlameHunk>, String)>,
|
||||
///
|
||||
pub commit_id: CommitId,
|
||||
///
|
||||
pub path: String,
|
||||
///
|
||||
pub lines: Vec<(Option<BlameHunk>, String)>,
|
||||
}
|
||||
|
||||
/// fixup `\` windows path separators to git compatible `/`
|
||||
fn fixup_windows_path(path: &str) -> String {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
path.replace('\\', "/")
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
path.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn blame_file(
|
||||
repo_path: &str,
|
||||
file_path: &str,
|
||||
repo_path: &RepoPath,
|
||||
file_path: &str,
|
||||
commit_id: Option<CommitId>,
|
||||
) -> Result<FileBlame> {
|
||||
scope_time!("blame_file");
|
||||
scope_time!("blame_file");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let commit_id = utils::get_head_repo(&repo)?;
|
||||
let commit_id = if let Some(commit_id) = commit_id {
|
||||
commit_id
|
||||
} else {
|
||||
utils::get_head_repo(&repo)?
|
||||
};
|
||||
|
||||
let spec = format!("{}:{}", commit_id.to_string(), file_path);
|
||||
let spec =
|
||||
format!("{}:{}", commit_id, fixup_windows_path(file_path));
|
||||
|
||||
let object = repo.revparse_single(&spec)?;
|
||||
let blob = repo.find_blob(object.id())?;
|
||||
let object = repo.revparse_single(&spec)?;
|
||||
let blob = repo.find_blob(object.id())?;
|
||||
|
||||
if blob.is_binary() {
|
||||
return Err(Error::NoBlameOnBinaryFile);
|
||||
}
|
||||
if blob.is_binary() {
|
||||
return Err(Error::NoBlameOnBinaryFile);
|
||||
}
|
||||
|
||||
let blame = repo.blame_file(Path::new(file_path), None)?;
|
||||
let mut opts = BlameOptions::new();
|
||||
opts.newest_commit(commit_id.into());
|
||||
|
||||
let reader = BufReader::new(blob.content());
|
||||
let blame =
|
||||
repo.blame_file(Path::new(file_path), Some(&mut opts))?;
|
||||
|
||||
let unique_commit_ids: HashSet<_> = blame
|
||||
.iter()
|
||||
.map(|hunk| CommitId::new(hunk.final_commit_id()))
|
||||
.collect();
|
||||
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
|
||||
commit_ids.extend(unique_commit_ids);
|
||||
let reader = BufReader::new(blob.content());
|
||||
|
||||
let commit_infos = get_commits_info(repo_path, &commit_ids, 0)?;
|
||||
let unique_commit_infos: HashMap<_, _> = commit_infos
|
||||
.iter()
|
||||
.map(|commit_info| (commit_info.id, commit_info))
|
||||
.collect();
|
||||
let unique_commit_ids: HashSet<_> = blame
|
||||
.iter()
|
||||
.map(|hunk| CommitId::new(hunk.final_commit_id()))
|
||||
.collect();
|
||||
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
|
||||
commit_ids.extend(unique_commit_ids);
|
||||
|
||||
let lines: Vec<(Option<BlameHunk>, String)> = reader
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
// Line indices in a `FileBlame` are 1-based.
|
||||
let corresponding_hunk = blame.get_line(i + 1);
|
||||
let commit_infos = get_commits_info(repo_path, &commit_ids, 0)?;
|
||||
let unique_commit_infos: HashMap<_, _> = commit_infos
|
||||
.iter()
|
||||
.map(|commit_info| (commit_info.id, commit_info))
|
||||
.collect();
|
||||
|
||||
if let Some(hunk) = corresponding_hunk {
|
||||
let commit_id = CommitId::new(hunk.final_commit_id());
|
||||
// Line indices in a `BlameHunk` are 1-based.
|
||||
let start_line =
|
||||
hunk.final_start_line().saturating_sub(1);
|
||||
let end_line =
|
||||
start_line.saturating_add(hunk.lines_in_hunk());
|
||||
let lines: Vec<(Option<BlameHunk>, String)> = reader
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
// Line indices in a `FileBlame` are 1-based.
|
||||
let corresponding_hunk = blame.get_line(i + 1);
|
||||
|
||||
if let Some(commit_info) =
|
||||
unique_commit_infos.get(&commit_id)
|
||||
{
|
||||
let hunk = BlameHunk {
|
||||
commit_id,
|
||||
author: commit_info.author.clone(),
|
||||
time: commit_info.time,
|
||||
start_line,
|
||||
end_line,
|
||||
};
|
||||
if let Some(hunk) = corresponding_hunk {
|
||||
let commit_id = CommitId::new(hunk.final_commit_id());
|
||||
// Line indices in a `BlameHunk` are 1-based.
|
||||
let start_line =
|
||||
hunk.final_start_line().saturating_sub(1);
|
||||
let end_line =
|
||||
start_line.saturating_add(hunk.lines_in_hunk());
|
||||
|
||||
return (
|
||||
Some(hunk),
|
||||
line.unwrap_or_else(|_| "".into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(commit_info) =
|
||||
unique_commit_infos.get(&commit_id)
|
||||
{
|
||||
let hunk = BlameHunk {
|
||||
commit_id,
|
||||
author: commit_info.author.clone(),
|
||||
time: commit_info.time,
|
||||
start_line,
|
||||
end_line,
|
||||
};
|
||||
|
||||
(None, line.unwrap_or_else(|_| "".into()))
|
||||
})
|
||||
.collect();
|
||||
return (
|
||||
Some(hunk),
|
||||
line.unwrap_or_else(|_| String::new()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let file_blame = FileBlame {
|
||||
commit_id,
|
||||
path: file_path.into(),
|
||||
lines,
|
||||
};
|
||||
(None, line.unwrap_or_else(|_| String::new()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(file_blame)
|
||||
let file_blame = FileBlame {
|
||||
commit_id,
|
||||
path: file_path.into(),
|
||||
lines,
|
||||
};
|
||||
|
||||
Ok(file_blame)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty,
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{commit, stage_add_file, tests::repo_init_empty},
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_blame() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_blame() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
assert!(matches!(blame_file(&repo_path, "foo"), Err(_)));
|
||||
assert!(blame_file(repo_path, "foo", None).is_err());
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"line 1\n")?;
|
||||
File::create(root.join(file_path))?.write_all(b"line 1\n")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "first commit")?;
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "first commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo")?;
|
||||
let blame = blame_file(repo_path, "foo", None)?;
|
||||
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
line
|
||||
)] if author == "name" && line == "line 1"
|
||||
));
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
line
|
||||
)] if author == "name" && line == "line 1"
|
||||
));
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&root.join(file_path))?;
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.open(root.join(file_path))?;
|
||||
|
||||
file.write(b"line 2\n")?;
|
||||
file.write(b"line 2\n")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "second commit")?;
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "second commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo")?;
|
||||
let blame = blame_file(repo_path, "foo", None)?;
|
||||
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[
|
||||
(
|
||||
Some(BlameHunk {
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
first_line
|
||||
),
|
||||
(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 1,
|
||||
end_line: 2,
|
||||
..
|
||||
}),
|
||||
second_line
|
||||
)
|
||||
] if author == "name" && first_line == "line 1" && second_line == "line 2"
|
||||
));
|
||||
assert!(matches!(
|
||||
blame.lines.as_slice(),
|
||||
[
|
||||
(
|
||||
Some(BlameHunk {
|
||||
start_line: 0,
|
||||
end_line: 1,
|
||||
..
|
||||
}),
|
||||
first_line
|
||||
),
|
||||
(
|
||||
Some(BlameHunk {
|
||||
author,
|
||||
start_line: 1,
|
||||
end_line: 2,
|
||||
..
|
||||
}),
|
||||
second_line
|
||||
)
|
||||
] if author == "name" && first_line == "line 1" && second_line == "line 2"
|
||||
));
|
||||
|
||||
file.write(b"line 3\n")?;
|
||||
file.write(b"line 3\n")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo")?;
|
||||
let blame = blame_file(repo_path, "foo", None)?;
|
||||
|
||||
assert_eq!(blame.lines.len(), 2);
|
||||
assert_eq!(blame.lines.len(), 2);
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "third commit")?;
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
commit(repo_path, "third commit")?;
|
||||
|
||||
let blame = blame_file(&repo_path, "foo")?;
|
||||
let blame = blame_file(repo_path, "foo", None)?;
|
||||
|
||||
assert_eq!(blame.lines.len(), 3);
|
||||
assert_eq!(blame.lines.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blame_windows_path_dividers() {
|
||||
let file_path = Path::new("bar\\foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
std::fs::create_dir(root.join("bar")).unwrap();
|
||||
|
||||
File::create(root.join(file_path))
|
||||
.unwrap()
|
||||
.write_all(b"line 1\n")
|
||||
.unwrap();
|
||||
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "first commit").unwrap();
|
||||
|
||||
assert!(blame_file(repo_path, "bar\\foo", None).is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use super::BranchType;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{merge_msg, utils, CommitId},
|
||||
error::{Error, Result},
|
||||
sync::{merge_msg, repository::repo, CommitId, RepoPath},
|
||||
};
|
||||
use git2::Commit;
|
||||
use scopetime::scope_time;
|
||||
|
|
@ -12,258 +12,270 @@ use scopetime::scope_time;
|
|||
/// if we did not create conflicts we create a merge commit and return the commit id.
|
||||
/// Otherwise we return `None`
|
||||
pub fn merge_upstream_commit(
|
||||
repo_path: &str,
|
||||
branch_name: &str,
|
||||
repo_path: &RepoPath,
|
||||
branch_name: &str,
|
||||
) -> Result<Option<CommitId>> {
|
||||
scope_time!("merge_upstream_commit");
|
||||
scope_time!("merge_upstream_commit");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let branch = repo.find_branch(branch_name, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
let branch = repo.find_branch(branch_name, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
|
||||
let upstream_commit = upstream.get().peel_to_commit()?;
|
||||
let upstream_commit = upstream.get().peel_to_commit()?;
|
||||
|
||||
let annotated_upstream = repo
|
||||
.reference_to_annotated_commit(&upstream.into_reference())?;
|
||||
let annotated_upstream = repo
|
||||
.reference_to_annotated_commit(&upstream.into_reference())?;
|
||||
|
||||
let (analysis, pref) =
|
||||
repo.merge_analysis(&[&annotated_upstream])?;
|
||||
let (analysis, pref) =
|
||||
repo.merge_analysis(&[&annotated_upstream])?;
|
||||
|
||||
if !analysis.is_normal() {
|
||||
return Err(Error::Generic(
|
||||
"normal merge not possible".into(),
|
||||
));
|
||||
}
|
||||
if !analysis.is_normal() {
|
||||
return Err(Error::Generic(
|
||||
"normal merge not possible".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if analysis.is_fast_forward() && pref.is_fastforward_only() {
|
||||
return Err(Error::Generic(
|
||||
"ff merge would be possible".into(),
|
||||
));
|
||||
}
|
||||
if analysis.is_fast_forward() && pref.is_fastforward_only() {
|
||||
return Err(Error::Generic(
|
||||
"ff merge would be possible".into(),
|
||||
));
|
||||
}
|
||||
|
||||
//TODO: support merge on unborn?
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
//TODO: support merge on unborn?
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
|
||||
repo.merge(&[&annotated_upstream], None, None)?;
|
||||
repo.merge(&[&annotated_upstream], None, None)?;
|
||||
|
||||
if !repo.index()?.has_conflicts() {
|
||||
let msg = merge_msg(repo_path)?;
|
||||
if !repo.index()?.has_conflicts() {
|
||||
let msg = merge_msg(repo_path)?;
|
||||
|
||||
let commit_id =
|
||||
commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
|
||||
let commit_id =
|
||||
commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
|
||||
|
||||
return Ok(Some(commit_id));
|
||||
}
|
||||
return Ok(Some(commit_id));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn commit_merge_with_head(
|
||||
repo: &git2::Repository,
|
||||
commits: &[Commit],
|
||||
msg: &str,
|
||||
repo: &git2::Repository,
|
||||
commits: &[Commit],
|
||||
msg: &str,
|
||||
) -> Result<CommitId> {
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(repo)?;
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
let head_commit = repo.find_commit(
|
||||
crate::sync::utils::get_head_repo(repo)?.into(),
|
||||
)?;
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(repo)?;
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
let head_commit = repo.find_commit(
|
||||
crate::sync::utils::get_head_repo(repo)?.into(),
|
||||
)?;
|
||||
|
||||
let mut parents = vec![&head_commit];
|
||||
parents.extend(commits);
|
||||
let mut parents = vec![&head_commit];
|
||||
parents.extend(commits);
|
||||
|
||||
let commit_id = repo
|
||||
.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)?
|
||||
.into();
|
||||
repo.cleanup_state()?;
|
||||
Ok(commit_id)
|
||||
let commit_id = repo
|
||||
.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)?
|
||||
.into();
|
||||
repo.cleanup_state()?;
|
||||
Ok(commit_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use git2::Time;
|
||||
use git2::Time;
|
||||
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
branch_compare_upstream,
|
||||
remotes::{fetch, push::push},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file, write_commit_file_at,
|
||||
},
|
||||
RepoState,
|
||||
};
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
branch_compare_upstream,
|
||||
remotes::{fetch, push::push_branch},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file, write_commit_file_at,
|
||||
},
|
||||
RepoState,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_merge_normal() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
#[test]
|
||||
fn test_merge_normal() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
// clone1
|
||||
|
||||
let commit1 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
Time::new(1, 0),
|
||||
);
|
||||
let commit1 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
Time::new(1, 0),
|
||||
);
|
||||
|
||||
push(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
push_branch(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
// clone2
|
||||
|
||||
let commit2 = write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
Time::new(2, 0),
|
||||
);
|
||||
let commit2 = write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
Time::new(2, 0),
|
||||
);
|
||||
|
||||
//push should fail since origin diverged
|
||||
assert!(push(
|
||||
clone2_dir, "origin", "master", false, None, None,
|
||||
)
|
||||
.is_err());
|
||||
//push should fail since origin diverged
|
||||
assert!(push_branch(
|
||||
&clone2_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
//lets fetch from origin
|
||||
let bytes = fetch(clone2_dir, "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
//lets fetch from origin
|
||||
let bytes =
|
||||
fetch(&clone2_dir.into(), "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
//we should be one commit behind
|
||||
assert_eq!(
|
||||
branch_compare_upstream(clone2_dir, "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
//we should be one commit behind
|
||||
assert_eq!(
|
||||
branch_compare_upstream(&clone2_dir.into(), "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
|
||||
let merge_commit =
|
||||
merge_upstream_commit(clone2_dir, "master")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let merge_commit =
|
||||
merge_upstream_commit(&clone2_dir.into(), "master")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let state = crate::sync::repo_state(clone2_dir).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
let state =
|
||||
crate::sync::repo_state(&clone2_dir.into()).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
assert!(!clone2.head_detached().unwrap());
|
||||
assert!(!clone2.head_detached().unwrap());
|
||||
|
||||
let commits = get_commit_ids(&clone2, 10);
|
||||
assert_eq!(commits.len(), 3);
|
||||
assert_eq!(commits[0], merge_commit);
|
||||
assert_eq!(commits[1], commit2);
|
||||
assert_eq!(commits[2], commit1);
|
||||
let commits = get_commit_ids(&clone2, 10);
|
||||
assert_eq!(commits.len(), 3);
|
||||
assert_eq!(commits[0], merge_commit);
|
||||
assert_eq!(commits[1], commit2);
|
||||
assert_eq!(commits[2], commit1);
|
||||
|
||||
//verify commit msg
|
||||
let details =
|
||||
crate::sync::get_commit_details(clone2_dir, merge_commit)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
//verify commit msg
|
||||
let details = crate::sync::get_commit_details(
|
||||
&clone2_dir.into(),
|
||||
merge_commit,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
details.message.unwrap().combine(),
|
||||
String::from("Merge remote-tracking branch 'refs/remotes/origin/master'")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_normal_non_ff() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
#[test]
|
||||
fn test_merge_normal_non_ff() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
// clone1
|
||||
// clone1
|
||||
|
||||
write_commit_file(
|
||||
&clone1,
|
||||
"test.bin",
|
||||
"test\nfooo",
|
||||
"commit1",
|
||||
);
|
||||
write_commit_file(
|
||||
&clone1,
|
||||
"test.bin",
|
||||
"test\nfooo",
|
||||
"commit1",
|
||||
);
|
||||
|
||||
debug_cmd_print(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
"git status",
|
||||
);
|
||||
debug_cmd_print(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
"git status",
|
||||
);
|
||||
|
||||
push(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
push_branch(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
// clone2
|
||||
|
||||
write_commit_file(
|
||||
&clone2,
|
||||
"test.bin",
|
||||
"foobar\ntest",
|
||||
"commit2",
|
||||
);
|
||||
write_commit_file(
|
||||
&clone2,
|
||||
"test.bin",
|
||||
"foobar\ntest",
|
||||
"commit2",
|
||||
);
|
||||
|
||||
let bytes = fetch(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(bytes > 0);
|
||||
let bytes = fetch(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
let res = merge_upstream_commit(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
"master",
|
||||
)
|
||||
.unwrap();
|
||||
let res = merge_upstream_commit(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
"master",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
//this should not have commited cause we left conflicts behind
|
||||
assert_eq!(res, None);
|
||||
//this should not have committed cause we left conflicts behind
|
||||
assert_eq!(res, None);
|
||||
|
||||
let state = crate::sync::repo_state(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let state = crate::sync::repo_state(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
//validate the repo is in a merge state now
|
||||
assert_eq!(state, RepoState::Merge);
|
||||
//validate the repo is in a merge state now
|
||||
assert_eq!(state, RepoState::Merge);
|
||||
|
||||
//check that we still only have the first commit
|
||||
let commits = get_commit_ids(&clone1, 10);
|
||||
assert_eq!(commits.len(), 1);
|
||||
}
|
||||
//check that we still only have the first commit
|
||||
let commits = get_commit_ids(&clone1, 10);
|
||||
assert_eq!(commits.len(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,141 +2,143 @@
|
|||
|
||||
use super::BranchType;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::utils,
|
||||
error::{Error, Result},
|
||||
sync::{repository::repo, RepoPath},
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
pub fn branch_merge_upstream_fastforward(
|
||||
repo_path: &str,
|
||||
branch: &str,
|
||||
repo_path: &RepoPath,
|
||||
branch: &str,
|
||||
) -> Result<()> {
|
||||
scope_time!("branch_merge_upstream");
|
||||
scope_time!("branch_merge_upstream");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let branch = repo.find_branch(branch, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
let branch = repo.find_branch(branch, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
|
||||
let upstream_commit =
|
||||
upstream.into_reference().peel_to_commit()?;
|
||||
let upstream_commit =
|
||||
upstream.into_reference().peel_to_commit()?;
|
||||
|
||||
let annotated =
|
||||
repo.find_annotated_commit(upstream_commit.id())?;
|
||||
let annotated =
|
||||
repo.find_annotated_commit(upstream_commit.id())?;
|
||||
|
||||
let (analysis, pref) = repo.merge_analysis(&[&annotated])?;
|
||||
let (analysis, pref) = repo.merge_analysis(&[&annotated])?;
|
||||
|
||||
if !analysis.is_fast_forward() {
|
||||
return Err(Error::Generic(
|
||||
"fast forward merge not possible".into(),
|
||||
));
|
||||
}
|
||||
if !analysis.is_fast_forward() {
|
||||
return Err(Error::Generic(
|
||||
"fast forward merge not possible".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if pref.is_no_fast_forward() {
|
||||
return Err(Error::Generic("fast forward not wanted".into()));
|
||||
}
|
||||
if pref.is_no_fast_forward() {
|
||||
return Err(Error::Generic("fast forward not wanted".into()));
|
||||
}
|
||||
|
||||
//TODO: support merge on unborn
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
//TODO: support merge on unborn
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
|
||||
repo.checkout_tree(upstream_commit.as_object(), None)?;
|
||||
repo.checkout_tree(upstream_commit.as_object(), None)?;
|
||||
|
||||
repo.head()?.set_target(annotated.id(), "")?;
|
||||
repo.head()?.set_target(annotated.id(), "")?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
remotes::{fetch, push::push},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file,
|
||||
},
|
||||
};
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
remotes::{fetch, push::push_branch},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_merge_fastforward() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
#[test]
|
||||
fn test_merge_fastforward() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
// clone1
|
||||
// clone1
|
||||
|
||||
let commit1 =
|
||||
write_commit_file(&clone1, "test.txt", "test", "commit1");
|
||||
let commit1 =
|
||||
write_commit_file(&clone1, "test.txt", "test", "commit1");
|
||||
|
||||
push(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
push_branch(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
debug_cmd_print(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
"git pull --ff",
|
||||
);
|
||||
// clone2
|
||||
debug_cmd_print(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
"git pull --ff",
|
||||
);
|
||||
|
||||
let commit2 = write_commit_file(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
);
|
||||
let commit2 = write_commit_file(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
);
|
||||
|
||||
push(
|
||||
clone2_dir.path().to_str().unwrap(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
push_branch(
|
||||
&clone2_dir.path().to_str().unwrap().into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone1 again
|
||||
// clone1 again
|
||||
|
||||
let bytes = fetch(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(bytes > 0);
|
||||
let bytes = fetch(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
let bytes = fetch(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bytes, 0);
|
||||
let bytes = fetch(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"master",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(bytes, 0);
|
||||
|
||||
branch_merge_upstream_fastforward(
|
||||
clone1_dir.path().to_str().unwrap(),
|
||||
"master",
|
||||
)
|
||||
.unwrap();
|
||||
branch_merge_upstream_fastforward(
|
||||
&clone1_dir.path().to_str().unwrap().into(),
|
||||
"master",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let commits = get_commit_ids(&clone1, 10);
|
||||
assert_eq!(commits.len(), 2);
|
||||
assert_eq!(commits[1], commit1);
|
||||
assert_eq!(commits[0], commit2);
|
||||
}
|
||||
let commits = get_commit_ids(&clone1, 10);
|
||||
assert_eq!(commits.len(), 2);
|
||||
assert_eq!(commits[1], commit1);
|
||||
assert_eq!(commits[0], commit2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,329 +1,356 @@
|
|||
//! merging from upstream (rebase)
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::utils,
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
rebase::conflict_free_rebase, repository::repo, CommitId,
|
||||
RepoPath,
|
||||
},
|
||||
};
|
||||
use git2::BranchType;
|
||||
use scopetime::scope_time;
|
||||
|
||||
/// trys merging current branch with its upstrema using rebase
|
||||
/// tries merging current branch with its upstream using rebase
|
||||
pub fn merge_upstream_rebase(
|
||||
repo_path: &str,
|
||||
branch_name: &str,
|
||||
) -> Result<()> {
|
||||
scope_time!("merge_upstream_rebase");
|
||||
repo_path: &RepoPath,
|
||||
branch_name: &str,
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("merge_upstream_rebase");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
if super::get_branch_name_repo(&repo)? != branch_name {
|
||||
return Err(Error::Generic(String::from(
|
||||
"can only rebase in head branch",
|
||||
)));
|
||||
}
|
||||
let repo = repo(repo_path)?;
|
||||
if super::get_branch_name_repo(&repo)? != branch_name {
|
||||
return Err(Error::Generic(String::from(
|
||||
"can only rebase in head branch",
|
||||
)));
|
||||
}
|
||||
|
||||
let branch = repo.find_branch(branch_name, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
let upstream_commit = upstream.get().peel_to_commit()?;
|
||||
let annotated_upstream =
|
||||
repo.find_annotated_commit(upstream_commit.id())?;
|
||||
let branch = repo.find_branch(branch_name, BranchType::Local)?;
|
||||
let upstream = branch.upstream()?;
|
||||
let upstream_commit = upstream.get().peel_to_commit()?;
|
||||
let annotated_upstream =
|
||||
repo.find_annotated_commit(upstream_commit.id())?;
|
||||
|
||||
let mut rebase =
|
||||
repo.rebase(None, Some(&annotated_upstream), None, None)?;
|
||||
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(&repo)?;
|
||||
|
||||
while let Some(op) = rebase.next() {
|
||||
let _op = op?;
|
||||
// dbg!(op.id());
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
rebase.abort()?;
|
||||
return Err(Error::Generic(String::from(
|
||||
"conflicts while merging",
|
||||
)));
|
||||
}
|
||||
|
||||
rebase.commit(None, &signature, None)?;
|
||||
}
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
rebase.abort()?;
|
||||
return Err(Error::Generic(String::from(
|
||||
"conflicts while merging",
|
||||
)));
|
||||
}
|
||||
|
||||
rebase.finish(Some(&signature))?;
|
||||
|
||||
Ok(())
|
||||
conflict_free_rebase(&repo, &annotated_upstream)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
branch_compare_upstream, get_commits_info,
|
||||
remotes::{fetch, push::push},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file, write_commit_file_at,
|
||||
},
|
||||
RepoState,
|
||||
};
|
||||
use git2::{Repository, Time};
|
||||
|
||||
fn get_commit_msgs(r: &Repository) -> Vec<String> {
|
||||
let commits = get_commit_ids(r, 10);
|
||||
get_commits_info(
|
||||
r.workdir().unwrap().to_str().unwrap(),
|
||||
&commits,
|
||||
10,
|
||||
)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|c| c.message)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_normal() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
branch_compare_upstream, get_commits_info,
|
||||
remotes::{fetch, push::push_branch},
|
||||
tests::{
|
||||
debug_cmd_print, get_commit_ids, repo_clone,
|
||||
repo_init_bare, write_commit_file, write_commit_file_at,
|
||||
},
|
||||
RepoState,
|
||||
};
|
||||
use git2::{Repository, Time};
|
||||
|
||||
fn get_commit_msgs(r: &Repository) -> Vec<String> {
|
||||
let commits = get_commit_ids(r, 10);
|
||||
get_commits_info(
|
||||
&r.workdir().unwrap().to_str().unwrap().into(),
|
||||
&commits,
|
||||
10,
|
||||
)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|c| c.message)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_normal() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit1 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
git2::Time::new(0, 0),
|
||||
);
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
|
||||
push_branch(
|
||||
&clone1_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
let _commit2 = write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
git2::Time::new(1, 0),
|
||||
);
|
||||
|
||||
assert!(!clone2.head_detached().unwrap());
|
||||
|
||||
push_branch(
|
||||
&clone2_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!clone2.head_detached().unwrap());
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit3 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test3.txt",
|
||||
"test",
|
||||
"commit3",
|
||||
git2::Time::new(2, 0),
|
||||
);
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
|
||||
//lets fetch from origin
|
||||
let bytes =
|
||||
fetch(&clone1_dir.into(), "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
//we should be one commit behind
|
||||
assert_eq!(
|
||||
branch_compare_upstream(&clone1_dir.into(), "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
|
||||
// debug_cmd_print(clone1_dir, "git status");
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
|
||||
merge_upstream_rebase(&clone1_dir.into(), "master").unwrap();
|
||||
|
||||
debug_cmd_print(&clone1_dir.into(), "git log");
|
||||
|
||||
let state =
|
||||
crate::sync::repo_state(&clone1_dir.into()).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![
|
||||
String::from("commit3"),
|
||||
String::from("commit2"),
|
||||
String::from("commit1")
|
||||
]
|
||||
);
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_multiple() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
Time::new(0, 0),
|
||||
);
|
||||
|
||||
push_branch(
|
||||
&clone1_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
Time::new(1, 0),
|
||||
);
|
||||
|
||||
push_branch(
|
||||
&clone2_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test3.txt",
|
||||
"test",
|
||||
"commit3",
|
||||
Time::new(2, 0),
|
||||
);
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test4.txt",
|
||||
"test",
|
||||
"commit4",
|
||||
Time::new(3, 0),
|
||||
);
|
||||
|
||||
//lets fetch from origin
|
||||
|
||||
fetch(&clone1_dir.into(), "master", None, None).unwrap();
|
||||
|
||||
merge_upstream_rebase(&clone1_dir.into(), "master").unwrap();
|
||||
|
||||
debug_cmd_print(&clone1_dir.into(), "git log");
|
||||
|
||||
let state =
|
||||
crate::sync::repo_state(&clone1_dir.into()).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![
|
||||
String::from("commit4"),
|
||||
String::from("commit3"),
|
||||
String::from("commit2"),
|
||||
String::from("commit1")
|
||||
]
|
||||
);
|
||||
|
||||
assert!(!clone1.head_detached().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_conflict() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit1 =
|
||||
write_commit_file(&clone1, "test.txt", "test", "commit1");
|
||||
|
||||
push_branch(
|
||||
&clone1_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
let _commit2 = write_commit_file(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
);
|
||||
|
||||
push_branch(
|
||||
&clone2_dir.into(),
|
||||
"origin",
|
||||
"master",
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit3 =
|
||||
write_commit_file(&clone1, "test2.txt", "foo", "commit3");
|
||||
|
||||
let bytes =
|
||||
fetch(&clone1_dir.into(), "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
assert_eq!(
|
||||
branch_compare_upstream(&clone1_dir.into(), "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
|
||||
let res = merge_upstream_rebase(&clone1_dir.into(), "master");
|
||||
assert!(res.is_err());
|
||||
|
||||
let state =
|
||||
crate::sync::repo_state(&clone1_dir.into()).unwrap();
|
||||
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit1 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
git2::Time::new(0, 0),
|
||||
);
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
|
||||
push(clone1_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
let _commit2 = write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
git2::Time::new(1, 0),
|
||||
);
|
||||
|
||||
assert_eq!(clone2.head_detached().unwrap(), false);
|
||||
|
||||
push(clone2_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(clone2.head_detached().unwrap(), false);
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit3 = write_commit_file_at(
|
||||
&clone1,
|
||||
"test3.txt",
|
||||
"test",
|
||||
"commit3",
|
||||
git2::Time::new(2, 0),
|
||||
);
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
|
||||
//lets fetch from origin
|
||||
let bytes = fetch(clone1_dir, "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
//we should be one commit behind
|
||||
assert_eq!(
|
||||
branch_compare_upstream(clone1_dir, "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
|
||||
// debug_cmd_print(clone1_dir, "git status");
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
|
||||
merge_upstream_rebase(clone1_dir, "master").unwrap();
|
||||
|
||||
debug_cmd_print(clone1_dir, "git log");
|
||||
|
||||
let state = crate::sync::repo_state(clone1_dir).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![
|
||||
String::from("commit3"),
|
||||
String::from("commit2"),
|
||||
String::from("commit1")
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_multiple() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test.txt",
|
||||
"test",
|
||||
"commit1",
|
||||
Time::new(0, 0),
|
||||
);
|
||||
|
||||
push(clone1_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
write_commit_file_at(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
Time::new(1, 0),
|
||||
);
|
||||
|
||||
push(clone2_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test3.txt",
|
||||
"test",
|
||||
"commit3",
|
||||
Time::new(2, 0),
|
||||
);
|
||||
write_commit_file_at(
|
||||
&clone1,
|
||||
"test4.txt",
|
||||
"test",
|
||||
"commit4",
|
||||
Time::new(3, 0),
|
||||
);
|
||||
|
||||
//lets fetch from origin
|
||||
|
||||
fetch(clone1_dir, "master", None, None).unwrap();
|
||||
|
||||
merge_upstream_rebase(clone1_dir, "master").unwrap();
|
||||
|
||||
debug_cmd_print(clone1_dir, "git log");
|
||||
|
||||
let state = crate::sync::repo_state(clone1_dir).unwrap();
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![
|
||||
String::from("commit4"),
|
||||
String::from("commit3"),
|
||||
String::from("commit2"),
|
||||
String::from("commit1")
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(clone1.head_detached().unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_conflict() {
|
||||
let (r1_dir, _repo) = repo_init_bare().unwrap();
|
||||
|
||||
let (clone1_dir, clone1) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone1_dir = clone1_dir.path().to_str().unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit1 =
|
||||
write_commit_file(&clone1, "test.txt", "test", "commit1");
|
||||
|
||||
push(clone1_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
// clone2
|
||||
|
||||
let (clone2_dir, clone2) =
|
||||
repo_clone(r1_dir.path().to_str().unwrap()).unwrap();
|
||||
|
||||
let clone2_dir = clone2_dir.path().to_str().unwrap();
|
||||
|
||||
let _commit2 = write_commit_file(
|
||||
&clone2,
|
||||
"test2.txt",
|
||||
"test",
|
||||
"commit2",
|
||||
);
|
||||
|
||||
push(clone2_dir, "origin", "master", false, None, None)
|
||||
.unwrap();
|
||||
|
||||
// clone1
|
||||
|
||||
let _commit3 =
|
||||
write_commit_file(&clone1, "test2.txt", "foo", "commit3");
|
||||
|
||||
let bytes = fetch(clone1_dir, "master", None, None).unwrap();
|
||||
assert!(bytes > 0);
|
||||
|
||||
assert_eq!(
|
||||
branch_compare_upstream(clone1_dir, "master")
|
||||
.unwrap()
|
||||
.behind,
|
||||
1
|
||||
);
|
||||
|
||||
let res = merge_upstream_rebase(clone1_dir, "master");
|
||||
assert!(res.is_err());
|
||||
|
||||
let state = crate::sync::repo_state(clone1_dir).unwrap();
|
||||
|
||||
assert_eq!(state, RepoState::Clean);
|
||||
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![String::from("commit3"), String::from("commit1")]
|
||||
);
|
||||
}
|
||||
let commits = get_commit_msgs(&clone1);
|
||||
assert_eq!(
|
||||
commits,
|
||||
vec![String::from("commit3"), String::from("commit1")]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,71 @@
|
|||
//! renaming of branches
|
||||
|
||||
use crate::{error::Result, sync::utils};
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{repository::repo, RepoPath},
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
|
||||
/// Rename the branch reference
|
||||
pub fn rename_branch(
|
||||
repo_path: &str,
|
||||
branch_ref: &str,
|
||||
new_name: &str,
|
||||
repo_path: &RepoPath,
|
||||
branch_ref: &str,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
scope_time!("delete_branch");
|
||||
scope_time!("rename_branch");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let branch_as_ref = repo.find_reference(branch_ref)?;
|
||||
let mut branch = git2::Branch::wrap(branch_as_ref);
|
||||
branch.rename(new_name, true)?;
|
||||
let repo = repo(repo_path)?;
|
||||
let branch_as_ref = repo.find_reference(branch_ref)?;
|
||||
let mut branch = git2::Branch::wrap(branch_as_ref);
|
||||
branch.rename(new_name, true)?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::*;
|
||||
use super::rename_branch;
|
||||
use crate::sync::tests::repo_init;
|
||||
use super::super::{checkout_branch, create_branch, RepoPath};
|
||||
use super::rename_branch;
|
||||
use crate::sync::tests::repo_init;
|
||||
|
||||
#[test]
|
||||
fn test_rename_branch() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_rename_branch() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
create_branch(repo_path, "branch1").unwrap();
|
||||
create_branch(repo_path, "branch1").unwrap();
|
||||
|
||||
checkout_branch(repo_path, "refs/heads/branch1").unwrap();
|
||||
checkout_branch(repo_path, "branch1").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repo.branches(None)
|
||||
.unwrap()
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0
|
||||
.name()
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"branch1"
|
||||
);
|
||||
assert_eq!(
|
||||
repo.branches(None)
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0
|
||||
.name()
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"branch1"
|
||||
);
|
||||
|
||||
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
|
||||
.unwrap();
|
||||
rename_branch(repo_path, "refs/heads/branch1", "AnotherName")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
repo.branches(None)
|
||||
.unwrap()
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0
|
||||
.name()
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"AnotherName"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
repo.branches(None)
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.0
|
||||
.name()
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"AnotherName"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,338 +1,548 @@
|
|||
use super::{utils::repo, CommitId};
|
||||
use crate::{error::Result, sync::utils::get_head_repo};
|
||||
use git2::{ErrorCode, ObjectType, Repository, Signature};
|
||||
//! Git Api for Commits
|
||||
use super::{CommitId, RepoPath};
|
||||
use crate::sync::sign::{SignBuilder, SignError};
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{repository::repo, utils::get_head_repo},
|
||||
};
|
||||
use git2::{
|
||||
message_prettify, ErrorCode, ObjectType, Repository, Signature,
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
pub fn amend(
|
||||
repo_path: &str,
|
||||
id: CommitId,
|
||||
msg: &str,
|
||||
repo_path: &RepoPath,
|
||||
id: CommitId,
|
||||
msg: &str,
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("amend");
|
||||
scope_time!("amend");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
let repo = repo(repo_path)?;
|
||||
let config = repo.config()?;
|
||||
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
|
||||
let new_id = commit.amend(
|
||||
Some("HEAD"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(msg),
|
||||
Some(&tree),
|
||||
)?;
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
|
||||
Ok(CommitId::new(new_id))
|
||||
if config.get_bool("commit.gpgsign").unwrap_or(false) {
|
||||
// HACK: we undo the last commit and create a new one
|
||||
use crate::sync::utils::undo_last_commit;
|
||||
|
||||
let head = get_head_repo(&repo)?;
|
||||
if head == commit.id().into() {
|
||||
undo_last_commit(repo_path)?;
|
||||
return self::commit(repo_path, msg);
|
||||
}
|
||||
|
||||
return Err(Error::SignAmendNonLastCommit);
|
||||
}
|
||||
|
||||
let committer = signature_allow_undefined_name(&repo)?;
|
||||
|
||||
let new_id = commit.amend(
|
||||
Some("HEAD"),
|
||||
None,
|
||||
Some(&committer), // Passing a value will overwrite the committer.
|
||||
None,
|
||||
Some(msg),
|
||||
Some(&tree),
|
||||
)?;
|
||||
|
||||
Ok(CommitId::new(new_id))
|
||||
}
|
||||
|
||||
/// Wrap `Repository::signature` to allow unknown user.name.
|
||||
///
|
||||
/// See <https://github.com/extrawurst/gitui/issues/79>.
|
||||
#[allow(clippy::redundant_pub_crate)]
|
||||
/// See <https://github.com/gitui-org/gitui/issues/79>.
|
||||
pub(crate) fn signature_allow_undefined_name(
|
||||
repo: &Repository,
|
||||
repo: &Repository,
|
||||
) -> std::result::Result<Signature<'_>, git2::Error> {
|
||||
let signature = repo.signature();
|
||||
let signature = repo.signature();
|
||||
|
||||
if let Err(ref e) = signature {
|
||||
if e.code() == ErrorCode::NotFound {
|
||||
let config = repo.config()?;
|
||||
if let Err(ref e) = signature {
|
||||
if e.code() == ErrorCode::NotFound {
|
||||
let config = repo.config()?;
|
||||
|
||||
if let (Err(_), Ok(email_entry)) = (
|
||||
config.get_entry("user.name"),
|
||||
config.get_entry("user.email"),
|
||||
) {
|
||||
if let Some(email) = email_entry.value() {
|
||||
return Signature::now("unknown", email);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if let (Err(_), Ok(email_entry)) = (
|
||||
config.get_entry("user.name"),
|
||||
config.get_entry("user.email"),
|
||||
) {
|
||||
if let Some(email) = email_entry.value() {
|
||||
return Signature::now("unknown", email);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
signature
|
||||
signature
|
||||
}
|
||||
|
||||
/// this does not run any git hooks
|
||||
pub fn commit(repo_path: &str, msg: &str) -> Result<CommitId> {
|
||||
scope_time!("commit");
|
||||
/// this does not run any git hooks, git-hooks have to be executed manually, checkout `hooks_commit_msg` for example
|
||||
pub fn commit(repo_path: &RepoPath, msg: &str) -> Result<CommitId> {
|
||||
scope_time!("commit");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
let config = repo.config()?;
|
||||
let signature = signature_allow_undefined_name(&repo)?;
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
|
||||
let signature = signature_allow_undefined_name(&repo)?;
|
||||
let mut index = repo.index()?;
|
||||
let tree_id = index.write_tree()?;
|
||||
let tree = repo.find_tree(tree_id)?;
|
||||
let parents = if let Ok(id) = get_head_repo(&repo) {
|
||||
vec![repo.find_commit(id.into())?]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let parents = if let Ok(id) = get_head_repo(&repo) {
|
||||
vec![repo.find_commit(id.into())?]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let parents = parents.iter().collect::<Vec<_>>();
|
||||
|
||||
let parents = parents.iter().collect::<Vec<_>>();
|
||||
let commit_id = if config
|
||||
.get_bool("commit.gpgsign")
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let buffer = repo.commit_create_buffer(
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)?;
|
||||
|
||||
Ok(repo
|
||||
.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)?
|
||||
.into())
|
||||
let commit = std::str::from_utf8(&buffer).map_err(|_e| {
|
||||
SignError::Shellout("utf8 conversion error".to_string())
|
||||
})?;
|
||||
|
||||
let signer = SignBuilder::from_gitconfig(&repo, &config)?;
|
||||
let (signature, signature_field) = signer.sign(&buffer)?;
|
||||
let commit_id = repo.commit_signed(
|
||||
commit,
|
||||
&signature,
|
||||
signature_field.as_deref(),
|
||||
)?;
|
||||
|
||||
// manually advance to the new commit ID
|
||||
// repo.commit does that on its own, repo.commit_signed does not
|
||||
// if there is no head, read default branch or default to "master"
|
||||
if let Ok(mut head) = repo.head() {
|
||||
head.set_target(commit_id, msg)?;
|
||||
} else {
|
||||
let default_branch_name = config
|
||||
.get_str("init.defaultBranch")
|
||||
.unwrap_or("master");
|
||||
repo.reference(
|
||||
&format!("refs/heads/{default_branch_name}"),
|
||||
commit_id,
|
||||
true,
|
||||
msg,
|
||||
)?;
|
||||
}
|
||||
|
||||
commit_id
|
||||
} else {
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(commit_id.into())
|
||||
}
|
||||
|
||||
/// Tag a commit.
|
||||
///
|
||||
/// This function will return an `Err(…)` variant if the tag’s name is refused
|
||||
/// by git or if the tag already exists.
|
||||
pub fn tag(
|
||||
repo_path: &str,
|
||||
commit_id: &CommitId,
|
||||
tag: &str,
|
||||
pub fn tag_commit(
|
||||
repo_path: &RepoPath,
|
||||
commit_id: &CommitId,
|
||||
tag: &str,
|
||||
message: Option<&str>,
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("tag");
|
||||
scope_time!("tag_commit");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let signature = signature_allow_undefined_name(&repo)?;
|
||||
let object_id = commit_id.get_oid();
|
||||
let target =
|
||||
repo.find_object(object_id, Some(ObjectType::Commit))?;
|
||||
let object_id = commit_id.get_oid();
|
||||
let target =
|
||||
repo.find_object(object_id, Some(ObjectType::Commit))?;
|
||||
|
||||
Ok(repo.tag(tag, &target, &signature, "", false)?.into())
|
||||
let c = if let Some(message) = message {
|
||||
let signature = signature_allow_undefined_name(&repo)?;
|
||||
repo.tag(tag, &target, &signature, message, false)?.into()
|
||||
} else {
|
||||
repo.tag_lightweight(tag, &target, false)?.into()
|
||||
};
|
||||
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
/// Loads the comment prefix from config & uses it to prettify commit messages
|
||||
pub fn commit_message_prettify(
|
||||
repo_path: &RepoPath,
|
||||
message: String,
|
||||
) -> Result<String> {
|
||||
let comment_char = repo(repo_path)?
|
||||
.config()?
|
||||
.get_string("core.commentChar")
|
||||
.ok()
|
||||
.and_then(|char_string| char_string.chars().next())
|
||||
.unwrap_or('#') as u8;
|
||||
|
||||
Ok(message_prettify(message, Some(comment_char))?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::error::Result;
|
||||
use crate::sync::tags::Tag;
|
||||
use crate::sync::RepoPath;
|
||||
use crate::sync::{
|
||||
commit, get_commit_details, get_commit_files, stage_add_file,
|
||||
tags::get_tags,
|
||||
tests::{get_statuses, repo_init, repo_init_empty},
|
||||
utils::get_head,
|
||||
LogWalker,
|
||||
};
|
||||
use commit::{amend, commit_message_prettify, tag_commit};
|
||||
use git2::Repository;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, get_commit_details, get_commit_files, stage_add_file,
|
||||
tags::get_tags,
|
||||
tests::{get_statuses, repo_init, repo_init_empty},
|
||||
utils::get_head,
|
||||
LogWalker,
|
||||
};
|
||||
use commit::{amend, tag};
|
||||
use git2::Repository;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
fn count_commits(repo: &Repository, max: usize) -> usize {
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(repo, max).unwrap();
|
||||
walk.read(&mut items).unwrap();
|
||||
items.len()
|
||||
}
|
||||
|
||||
fn count_commits(repo: &Repository, max: usize) -> usize {
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo, max).unwrap();
|
||||
walk.read(&mut items).unwrap();
|
||||
items.len()
|
||||
}
|
||||
#[test]
|
||||
fn test_commit() {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
fn test_commit() {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
File::create(root.join(file_path))
|
||||
.unwrap()
|
||||
.write_all(b"test\nfoo")
|
||||
.unwrap();
|
||||
|
||||
File::create(&root.join(file_path))
|
||||
.unwrap()
|
||||
.write_all(b"test\nfoo")
|
||||
.unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (1, 0));
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (1, 0));
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (0, 1));
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 1));
|
||||
commit(repo_path, "commit msg").unwrap();
|
||||
|
||||
commit(repo_path, "commit msg").unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
}
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
}
|
||||
#[test]
|
||||
fn test_commit_in_empty_repo() {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
fn test_commit_in_empty_repo() {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
File::create(root.join(file_path))
|
||||
.unwrap()
|
||||
.write_all(b"test\nfoo")
|
||||
.unwrap();
|
||||
|
||||
File::create(&root.join(file_path))
|
||||
.unwrap()
|
||||
.write_all(b"test\nfoo")
|
||||
.unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (1, 0));
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (1, 0));
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (0, 1));
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 1));
|
||||
commit(repo_path, "commit msg").unwrap();
|
||||
|
||||
commit(repo_path, "commit msg").unwrap();
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
}
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (0, 0));
|
||||
}
|
||||
#[test]
|
||||
fn test_amend() -> Result<()> {
|
||||
let file_path1 = Path::new("foo");
|
||||
let file_path2 = Path::new("foo2");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
fn test_amend() -> Result<()> {
|
||||
let file_path1 = Path::new("foo");
|
||||
let file_path2 = Path::new("foo2");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
File::create(root.join(file_path1))?.write_all(b"test1")?;
|
||||
|
||||
File::create(&root.join(file_path1))?.write_all(b"test1")?;
|
||||
stage_add_file(repo_path, file_path1)?;
|
||||
let id = commit(repo_path, "commit msg")?;
|
||||
|
||||
stage_add_file(repo_path, file_path1)?;
|
||||
let id = commit(repo_path, "commit msg")?;
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
File::create(root.join(file_path2))?.write_all(b"test2")?;
|
||||
|
||||
File::create(&root.join(file_path2))?.write_all(b"test2")?;
|
||||
stage_add_file(repo_path, file_path2)?;
|
||||
|
||||
stage_add_file(repo_path, file_path2)?;
|
||||
let new_id = amend(repo_path, id, "amended")?;
|
||||
|
||||
let new_id = amend(repo_path, id, "amended")?;
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
let details = get_commit_details(repo_path, new_id)?;
|
||||
assert_eq!(details.message.unwrap().subject, "amended");
|
||||
|
||||
let details = get_commit_details(repo_path, new_id)?;
|
||||
assert_eq!(details.message.unwrap().subject, "amended");
|
||||
let files = get_commit_files(repo_path, new_id, None)?;
|
||||
|
||||
let files = get_commit_files(repo_path, new_id)?;
|
||||
assert_eq!(files.len(), 2);
|
||||
|
||||
assert_eq!(files.len(), 2);
|
||||
let head = get_head(repo_path)?;
|
||||
|
||||
let head = get_head(repo_path)?;
|
||||
assert_eq!(head, new_id);
|
||||
|
||||
assert_eq!(head, new_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_amend_with_different_user() {
|
||||
let file_path1 = Path::new("foo");
|
||||
let file_path2 = Path::new("foo2");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
fn test_tag() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
File::create(root.join(file_path1))
|
||||
.unwrap()
|
||||
.write_all(b"test1")
|
||||
.unwrap();
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
stage_add_file(repo_path, file_path1).unwrap();
|
||||
let id = commit(repo_path, "commit msg").unwrap();
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
let amended_details =
|
||||
get_commit_details(repo_path, id).unwrap();
|
||||
|
||||
let new_id = commit(repo_path, "commit msg")?;
|
||||
assert_eq!(amended_details.committer, None);
|
||||
|
||||
tag(repo_path, &new_id, "tag")?;
|
||||
File::create(root.join(file_path2))
|
||||
.unwrap()
|
||||
.write_all(b"test2")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec!["tag"]
|
||||
);
|
||||
stage_add_file(repo_path, file_path2).unwrap();
|
||||
|
||||
assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_)));
|
||||
repo.config()
|
||||
.unwrap()
|
||||
.set_str("user.name", "changed name")
|
||||
.unwrap();
|
||||
repo.config()
|
||||
.unwrap()
|
||||
.set_str("user.email", "changed@example.com")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec!["tag"]
|
||||
);
|
||||
let new_id = amend(repo_path, id, "amended").unwrap();
|
||||
|
||||
tag(repo_path, &new_id, "second-tag")?;
|
||||
let amended_details =
|
||||
get_commit_details(repo_path, new_id).unwrap();
|
||||
assert_eq!(amended_details.author.name, "name");
|
||||
assert_eq!(amended_details.author.email, "email");
|
||||
let committer = amended_details.committer.unwrap();
|
||||
assert_eq!(committer.name, "changed name");
|
||||
assert_eq!(committer.email, "changed@example.com");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec!["second-tag", "tag"]
|
||||
);
|
||||
#[test]
|
||||
fn test_tag() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
/// Beware: this test has to be run with a `$HOME/.gitconfig` that has
|
||||
/// `user.email` not set. Otherwise, git falls back to the value of
|
||||
/// `user.email` in `$HOME/.gitconfig` and this test fails.
|
||||
///
|
||||
/// As of February 2021, `repo_init_empty` sets all git config locations
|
||||
/// to an empty temporary directory, so this constraint is met.
|
||||
#[test]
|
||||
fn test_empty_email() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
let new_id = commit(repo_path, "commit msg")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
tag_commit(repo_path, &new_id, "tag", None)?;
|
||||
|
||||
repo.config()?.remove("user.email")?;
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec![Tag::new("tag")]
|
||||
);
|
||||
|
||||
let error = commit(repo_path, "commit msg");
|
||||
assert!(tag_commit(repo_path, &new_id, "tag", None).is_err());
|
||||
|
||||
assert!(matches!(error, Err(_)));
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec![Tag::new("tag")]
|
||||
);
|
||||
|
||||
repo.config()?.set_str("user.email", "email")?;
|
||||
tag_commit(repo_path, &new_id, "second-tag", None)?;
|
||||
|
||||
let success = commit(repo_path, "commit msg");
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id],
|
||||
vec![Tag::new("second-tag"), Tag::new("tag")]
|
||||
);
|
||||
|
||||
assert!(matches!(success, Ok(_)));
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
#[test]
|
||||
fn test_tag_with_message() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
assert_eq!(details.author.name, "name");
|
||||
assert_eq!(details.author.email, "email");
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
|
||||
/// See comment to `test_empty_email`.
|
||||
#[test]
|
||||
fn test_empty_name() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
let new_id = commit(repo_path, "commit msg")?;
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
tag_commit(repo_path, &new_id, "tag", Some("tag-message"))?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
assert_eq!(
|
||||
get_tags(repo_path).unwrap()[&new_id][0]
|
||||
.annotation
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
"tag-message"
|
||||
);
|
||||
|
||||
repo.config()?.remove("user.name")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut success = commit(repo_path, "commit msg");
|
||||
/// Beware: this test has to be run with a `$HOME/.gitconfig` that has
|
||||
/// `user.email` not set. Otherwise, git falls back to the value of
|
||||
/// `user.email` in `$HOME/.gitconfig` and this test fails.
|
||||
///
|
||||
/// As of February 2021, `repo_init_empty` sets all git config locations
|
||||
/// to an empty temporary directory, so this constraint is met.
|
||||
#[test]
|
||||
fn test_empty_email() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
assert!(matches!(success, Ok(_)));
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
let mut details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
|
||||
assert_eq!(details.author.name, "unknown");
|
||||
assert_eq!(details.author.email, "email");
|
||||
repo.config()?.remove("user.email")?;
|
||||
|
||||
repo.config()?.set_str("user.name", "name")?;
|
||||
let error = commit(repo_path, "commit msg");
|
||||
|
||||
success = commit(repo_path, "commit msg");
|
||||
assert!(error.is_err());
|
||||
|
||||
assert!(matches!(success, Ok(_)));
|
||||
assert_eq!(count_commits(&repo, 10), 2);
|
||||
repo.config()?.set_str("user.email", "email")?;
|
||||
|
||||
details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
let success = commit(repo_path, "commit msg");
|
||||
|
||||
assert_eq!(details.author.name, "name");
|
||||
assert_eq!(details.author.email, "email");
|
||||
assert!(success.is_ok());
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
|
||||
assert_eq!(details.author.name, "name");
|
||||
assert_eq!(details.author.email, "email");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See comment to `test_empty_email`.
|
||||
#[test]
|
||||
fn test_empty_name() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test\nfoo")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
|
||||
repo.config()?.remove("user.name")?;
|
||||
|
||||
let mut success = commit(repo_path, "commit msg");
|
||||
|
||||
assert!(success.is_ok());
|
||||
assert_eq!(count_commits(&repo, 10), 1);
|
||||
|
||||
let mut details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
|
||||
assert_eq!(details.author.name, "unknown");
|
||||
assert_eq!(details.author.email, "email");
|
||||
|
||||
repo.config()?.set_str("user.name", "name")?;
|
||||
|
||||
success = commit(repo_path, "commit msg");
|
||||
|
||||
assert!(success.is_ok());
|
||||
assert_eq!(count_commits(&repo, 10), 2);
|
||||
|
||||
details =
|
||||
get_commit_details(repo_path, success.unwrap()).unwrap();
|
||||
|
||||
assert_eq!(details.author.name, "name");
|
||||
assert_eq!(details.author.email, "email");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_comment_char() -> Result<()> {
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let message = commit_message_prettify(
|
||||
repo_path,
|
||||
"#This is a test message\nTest".to_owned(),
|
||||
)?;
|
||||
|
||||
assert_eq!(message, "Test\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_comment_char() -> Result<()> {
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
repo.config()?.set_str("core.commentChar", ";")?;
|
||||
|
||||
let message = commit_message_prettify(
|
||||
repo_path,
|
||||
";This is a test message\nTest".to_owned(),
|
||||
)?;
|
||||
|
||||
assert_eq!(message, "Test\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,171 +1,219 @@
|
|||
use super::{commits_info::get_message, utils::repo, CommitId};
|
||||
use crate::error::Result;
|
||||
use super::{commits_info::get_message, CommitId, RepoPath};
|
||||
use crate::{error::Result, sync::repository::repo};
|
||||
use git2::Signature;
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Default, Clone)]
|
||||
pub struct CommitSignature {
|
||||
///
|
||||
pub name: String,
|
||||
///
|
||||
pub email: String,
|
||||
/// time in secs since Unix epoch
|
||||
pub time: i64,
|
||||
///
|
||||
pub name: String,
|
||||
///
|
||||
pub email: String,
|
||||
/// time in secs since Unix epoch
|
||||
pub time: i64,
|
||||
}
|
||||
|
||||
impl CommitSignature {
|
||||
/// convert from git2-rs `Signature`
|
||||
pub fn from(s: &Signature<'_>) -> Self {
|
||||
Self {
|
||||
name: s.name().unwrap_or("").to_string(),
|
||||
email: s.email().unwrap_or("").to_string(),
|
||||
/// convert from git2-rs `Signature`
|
||||
pub fn from(s: &Signature<'_>) -> Self {
|
||||
Self {
|
||||
name: s.name().unwrap_or("").to_string(),
|
||||
email: s.email().unwrap_or("").to_string(),
|
||||
|
||||
time: s.when().seconds(),
|
||||
}
|
||||
}
|
||||
time: s.when().seconds(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CommitMessage {
|
||||
/// first line
|
||||
pub subject: String,
|
||||
/// remaining lines if more than one
|
||||
pub body: Option<String>,
|
||||
/// first line
|
||||
pub subject: String,
|
||||
/// remaining lines if more than one
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
impl CommitMessage {
|
||||
///
|
||||
pub fn from(s: &str) -> Self {
|
||||
let mut lines = s.lines();
|
||||
let subject = lines.next().map_or_else(
|
||||
String::new,
|
||||
std::string::ToString::to_string,
|
||||
);
|
||||
///
|
||||
pub fn from(s: &str) -> Self {
|
||||
let mut lines = s.lines();
|
||||
let subject = lines.next().map_or_else(
|
||||
String::new,
|
||||
std::string::ToString::to_string,
|
||||
);
|
||||
|
||||
let body: Vec<String> =
|
||||
lines.map(std::string::ToString::to_string).collect();
|
||||
let body: Vec<String> =
|
||||
lines.map(std::string::ToString::to_string).collect();
|
||||
|
||||
Self {
|
||||
subject,
|
||||
body: if body.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(body.join("\n"))
|
||||
},
|
||||
}
|
||||
}
|
||||
Self {
|
||||
subject,
|
||||
body: if body.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(body.join("\n"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn combine(self) -> String {
|
||||
if let Some(body) = self.body {
|
||||
format!("{}\n{}", self.subject, body)
|
||||
} else {
|
||||
self.subject
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn combine(self) -> String {
|
||||
if let Some(body) = self.body {
|
||||
format!("{}\n{body}", self.subject)
|
||||
} else {
|
||||
self.subject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CommitDetails {
|
||||
///
|
||||
pub author: CommitSignature,
|
||||
/// committer when differs to `author` otherwise None
|
||||
pub committer: Option<CommitSignature>,
|
||||
///
|
||||
pub message: Option<CommitMessage>,
|
||||
///
|
||||
pub hash: String,
|
||||
///
|
||||
pub author: CommitSignature,
|
||||
/// committer when differs to `author` otherwise None
|
||||
pub committer: Option<CommitSignature>,
|
||||
///
|
||||
pub message: Option<CommitMessage>,
|
||||
///
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
impl CommitDetails {
|
||||
///
|
||||
pub fn short_hash(&self) -> &str {
|
||||
&self.hash[0..7]
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the author of a commit.
|
||||
pub fn get_author_of_commit<'a>(
|
||||
commit: &'a git2::Commit<'a>,
|
||||
mailmap: &git2::Mailmap,
|
||||
) -> git2::Signature<'a> {
|
||||
match commit.author_with_mailmap(mailmap) {
|
||||
Ok(author) => author,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Couldn't get author with mailmap for {} (message: {:?}): {e}",
|
||||
commit.id(),
|
||||
commit.message(),
|
||||
);
|
||||
commit.author()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the committer of a commit.
|
||||
pub fn get_committer_of_commit<'a>(
|
||||
commit: &'a git2::Commit<'a>,
|
||||
mailmap: &git2::Mailmap,
|
||||
) -> git2::Signature<'a> {
|
||||
match commit.committer_with_mailmap(mailmap) {
|
||||
Ok(committer) => committer,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Couldn't get committer with mailmap for {} (message: {:?}): {e}",
|
||||
commit.id(),
|
||||
commit.message(),
|
||||
);
|
||||
commit.committer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_commit_details(
|
||||
repo_path: &str,
|
||||
id: CommitId,
|
||||
repo_path: &RepoPath,
|
||||
id: CommitId,
|
||||
) -> Result<CommitDetails> {
|
||||
scope_time!("get_commit_details");
|
||||
scope_time!("get_commit_details");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
let mailmap = repo.mailmap()?;
|
||||
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
|
||||
let author = CommitSignature::from(&commit.author());
|
||||
let committer = CommitSignature::from(&commit.committer());
|
||||
let committer = if author == committer {
|
||||
None
|
||||
} else {
|
||||
Some(committer)
|
||||
};
|
||||
let author = CommitSignature::from(&get_author_of_commit(
|
||||
&commit, &mailmap,
|
||||
));
|
||||
let committer = CommitSignature::from(&get_committer_of_commit(
|
||||
&commit, &mailmap,
|
||||
));
|
||||
|
||||
let msg =
|
||||
CommitMessage::from(get_message(&commit, None).as_str());
|
||||
let committer = if author == committer {
|
||||
None
|
||||
} else {
|
||||
Some(committer)
|
||||
};
|
||||
|
||||
let details = CommitDetails {
|
||||
author,
|
||||
committer,
|
||||
message: Some(msg),
|
||||
hash: id.to_string(),
|
||||
};
|
||||
let msg =
|
||||
CommitMessage::from(get_message(&commit, None).as_str());
|
||||
|
||||
Ok(details)
|
||||
let details = CommitDetails {
|
||||
author,
|
||||
committer,
|
||||
message: Some(msg),
|
||||
hash: id.to_string(),
|
||||
};
|
||||
|
||||
Ok(details)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{get_commit_details, CommitMessage};
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty, RepoPath,
|
||||
},
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
use super::{get_commit_details, CommitMessage};
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty,
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
#[test]
|
||||
fn test_msg_invalid_utf8() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
fn test_msg_invalid_utf8() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let msg = invalidstring::invalid_utf8("test msg");
|
||||
let id = commit(repo_path, msg.as_str()).unwrap();
|
||||
|
||||
let msg = invalidstring::invalid_utf8("test msg");
|
||||
let id = commit(repo_path, msg.as_str()).unwrap();
|
||||
let res = get_commit_details(repo_path, id).unwrap();
|
||||
|
||||
let res = get_commit_details(repo_path, id).unwrap();
|
||||
assert!(res
|
||||
.message
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.subject
|
||||
.starts_with("test msg"));
|
||||
|
||||
dbg!(&res.message.as_ref().unwrap().subject);
|
||||
assert_eq!(
|
||||
res.message
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.subject
|
||||
.starts_with("test msg"),
|
||||
true
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_msg_linefeeds() -> Result<()> {
|
||||
let msg = CommitMessage::from("foo\nbar\r\ntest");
|
||||
|
||||
#[test]
|
||||
fn test_msg_linefeeds() -> Result<()> {
|
||||
let msg = CommitMessage::from("foo\nbar\r\ntest");
|
||||
assert_eq!(msg.subject, String::from("foo"),);
|
||||
assert_eq!(msg.body, Some(String::from("bar\ntest")),);
|
||||
|
||||
assert_eq!(msg.subject, String::from("foo"),);
|
||||
assert_eq!(msg.body, Some(String::from("bar\ntest")),);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_commit_message_combine() -> Result<()> {
|
||||
let msg = CommitMessage::from("foo\nbar\r\ntest");
|
||||
|
||||
#[test]
|
||||
fn test_commit_message_combine() -> Result<()> {
|
||||
let msg = CommitMessage::from("foo\nbar\r\ntest");
|
||||
assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
|
||||
|
||||
assert_eq!(msg.combine(), String::from("foo\nbar\ntest"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,173 +1,269 @@
|
|||
use super::{stash::is_stash_commit, utils::repo, CommitId};
|
||||
//! Functions for getting infos about files in commits
|
||||
|
||||
use super::{diff::DiffOptions, CommitId, RepoPath};
|
||||
use crate::{
|
||||
error::Error, error::Result, StatusItem, StatusItemType,
|
||||
error::Result,
|
||||
sync::{get_stashes, repository::repo},
|
||||
StatusItem, StatusItemType,
|
||||
};
|
||||
use git2::{Diff, DiffDelta, DiffOptions, Repository};
|
||||
use git2::{Diff, Repository};
|
||||
use scopetime::scope_time;
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// struct containing a new and an old version
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct OldNew<T> {
|
||||
/// The old version
|
||||
pub old: T,
|
||||
/// The new version
|
||||
pub new: T,
|
||||
}
|
||||
|
||||
/// Sort two commits.
|
||||
pub fn sort_commits(
|
||||
repo: &Repository,
|
||||
commits: (CommitId, CommitId),
|
||||
) -> Result<OldNew<CommitId>> {
|
||||
if repo.graph_descendant_of(
|
||||
commits.0.get_oid(),
|
||||
commits.1.get_oid(),
|
||||
)? {
|
||||
Ok(OldNew {
|
||||
old: commits.1,
|
||||
new: commits.0,
|
||||
})
|
||||
} else {
|
||||
Ok(OldNew {
|
||||
old: commits.0,
|
||||
new: commits.1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// get all files that are part of a commit
|
||||
pub fn get_commit_files(
|
||||
repo_path: &str,
|
||||
id: CommitId,
|
||||
repo_path: &RepoPath,
|
||||
id: CommitId,
|
||||
other: Option<CommitId>,
|
||||
) -> Result<Vec<StatusItem>> {
|
||||
scope_time!("get_commit_files");
|
||||
scope_time!("get_commit_files");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_commit_diff(&repo, id, None)?;
|
||||
let diff = if let Some(other) = other {
|
||||
get_compare_commits_diff(
|
||||
&repo,
|
||||
sort_commits(&repo, (id, other))?,
|
||||
None,
|
||||
None,
|
||||
)?
|
||||
} else {
|
||||
get_commit_diff(
|
||||
&repo,
|
||||
id,
|
||||
None,
|
||||
None,
|
||||
Some(&get_stashes(repo_path)?.into_iter().collect()),
|
||||
)?
|
||||
};
|
||||
|
||||
let mut res = Vec::new();
|
||||
let res = diff
|
||||
.deltas()
|
||||
.map(|delta| {
|
||||
let status = StatusItemType::from(delta.status());
|
||||
|
||||
diff.foreach(
|
||||
&mut |delta: DiffDelta<'_>, _progress| {
|
||||
res.push(StatusItem {
|
||||
path: delta
|
||||
.new_file()
|
||||
.path()
|
||||
.map(|p| p.to_str().unwrap_or("").to_string())
|
||||
.unwrap_or_default(),
|
||||
status: StatusItemType::from(delta.status()),
|
||||
});
|
||||
true
|
||||
},
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
StatusItem {
|
||||
path: delta
|
||||
.new_file()
|
||||
.path()
|
||||
.map(|p| p.to_str().unwrap_or("").to_string())
|
||||
.unwrap_or_default(),
|
||||
status,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(res)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_pub_crate)]
|
||||
pub(crate) fn get_commit_diff(
|
||||
repo: &Repository,
|
||||
id: CommitId,
|
||||
pathspec: Option<String>,
|
||||
/// get diff of two arbitrary commits
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn get_compare_commits_diff(
|
||||
repo: &Repository,
|
||||
ids: OldNew<CommitId>,
|
||||
pathspec: Option<String>,
|
||||
options: Option<DiffOptions>,
|
||||
) -> Result<Diff<'_>> {
|
||||
// scope_time!("get_commit_diff");
|
||||
// scope_time!("get_compare_commits_diff");
|
||||
let commits = OldNew {
|
||||
old: repo.find_commit(ids.old.into())?,
|
||||
new: repo.find_commit(ids.new.into())?,
|
||||
};
|
||||
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
let commit_tree = commit.tree()?;
|
||||
let parent = if commit.parent_count() > 0 {
|
||||
Some(repo.find_commit(commit.parent_id(0)?)?.tree()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let trees = OldNew {
|
||||
old: commits.old.tree()?,
|
||||
new: commits.new.tree()?,
|
||||
};
|
||||
|
||||
let mut opts = DiffOptions::new();
|
||||
if let Some(p) = &pathspec {
|
||||
opts.pathspec(p.clone());
|
||||
}
|
||||
opts.show_binary(true);
|
||||
let mut opts = git2::DiffOptions::new();
|
||||
if let Some(options) = options {
|
||||
opts.context_lines(options.context);
|
||||
opts.ignore_whitespace(options.ignore_whitespace);
|
||||
opts.interhunk_lines(options.interhunk_lines);
|
||||
}
|
||||
if let Some(p) = &pathspec {
|
||||
opts.pathspec(p.clone());
|
||||
}
|
||||
|
||||
let mut diff = repo.diff_tree_to_tree(
|
||||
parent.as_ref(),
|
||||
Some(&commit_tree),
|
||||
Some(&mut opts),
|
||||
)?;
|
||||
let diff: Diff<'_> = repo.diff_tree_to_tree(
|
||||
Some(&trees.old),
|
||||
Some(&trees.new),
|
||||
Some(&mut opts),
|
||||
)?;
|
||||
|
||||
if is_stash_commit(
|
||||
repo.path().to_str().map_or_else(
|
||||
|| Err(Error::Generic("repo path utf8 err".to_owned())),
|
||||
Ok,
|
||||
)?,
|
||||
&id,
|
||||
)? {
|
||||
if let Ok(untracked_commit) = commit.parent_id(2) {
|
||||
let untracked_diff = get_commit_diff(
|
||||
repo,
|
||||
CommitId::new(untracked_commit),
|
||||
pathspec,
|
||||
)?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
diff.merge(&untracked_diff)?;
|
||||
}
|
||||
}
|
||||
/// get diff of a commit to its first parent
|
||||
pub(crate) fn get_commit_diff<'a>(
|
||||
repo: &'a Repository,
|
||||
id: CommitId,
|
||||
pathspec: Option<String>,
|
||||
options: Option<DiffOptions>,
|
||||
stashes: Option<&HashSet<CommitId>>,
|
||||
) -> Result<Diff<'a>> {
|
||||
// scope_time!("get_commit_diff");
|
||||
|
||||
Ok(diff)
|
||||
let commit = repo.find_commit(id.into())?;
|
||||
let commit_tree = commit.tree()?;
|
||||
|
||||
let parent = if commit.parent_count() > 0 {
|
||||
repo.find_commit(commit.parent_id(0)?)
|
||||
.ok()
|
||||
.and_then(|c| c.tree().ok())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut opts = git2::DiffOptions::new();
|
||||
if let Some(options) = options {
|
||||
opts.context_lines(options.context);
|
||||
opts.ignore_whitespace(options.ignore_whitespace);
|
||||
opts.interhunk_lines(options.interhunk_lines);
|
||||
}
|
||||
if let Some(p) = &pathspec {
|
||||
opts.pathspec(p.clone());
|
||||
}
|
||||
opts.show_binary(true);
|
||||
|
||||
let mut diff = repo.diff_tree_to_tree(
|
||||
parent.as_ref(),
|
||||
Some(&commit_tree),
|
||||
Some(&mut opts),
|
||||
)?;
|
||||
|
||||
if stashes.is_some_and(|stashes| stashes.contains(&id)) {
|
||||
if let Ok(untracked_commit) = commit.parent_id(2) {
|
||||
let untracked_diff = get_commit_diff(
|
||||
repo,
|
||||
CommitId::new(untracked_commit),
|
||||
pathspec,
|
||||
options,
|
||||
stashes,
|
||||
)?;
|
||||
|
||||
diff.merge(&untracked_diff)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_commit_files;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
commit, stage_add_file, stash_save,
|
||||
tests::{get_statuses, repo_init},
|
||||
},
|
||||
StatusItemType,
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use super::get_commit_files;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
commit, stage_add_file, stash_save,
|
||||
tests::{get_statuses, repo_init},
|
||||
RepoPath,
|
||||
},
|
||||
StatusItemType,
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_smoke() -> Result<()> {
|
||||
let file_path = Path::new("file1.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_smoke() -> Result<()> {
|
||||
let file_path = Path::new("file1.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"test file1 content")?;
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test file1 content")?;
|
||||
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
stage_add_file(repo_path, file_path)?;
|
||||
|
||||
let id = commit(repo_path, "commit msg")?;
|
||||
let id = commit(repo_path, "commit msg")?;
|
||||
|
||||
let diff = get_commit_files(repo_path, id)?;
|
||||
let diff = get_commit_files(repo_path, id, None)?;
|
||||
|
||||
assert_eq!(diff.len(), 1);
|
||||
assert_eq!(diff[0].status, StatusItemType::New);
|
||||
assert_eq!(diff.len(), 1);
|
||||
assert_eq!(diff[0].status, StatusItemType::New);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stashed_untracked() -> Result<()> {
|
||||
let file_path = Path::new("file1.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_stashed_untracked() -> Result<()> {
|
||||
let file_path = Path::new("file1.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?
|
||||
.write_all(b"test file1 content")?;
|
||||
File::create(root.join(file_path))?
|
||||
.write_all(b"test file1 content")?;
|
||||
|
||||
let id = stash_save(repo_path, None, true, false)?;
|
||||
let id = stash_save(repo_path, None, true, false)?;
|
||||
|
||||
let diff = get_commit_files(repo_path, id)?;
|
||||
let diff = get_commit_files(repo_path, id, None)?;
|
||||
|
||||
assert_eq!(diff.len(), 1);
|
||||
assert_eq!(diff[0].status, StatusItemType::New);
|
||||
assert_eq!(diff.len(), 1);
|
||||
assert_eq!(diff[0].status, StatusItemType::New);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stashed_untracked_and_modified() -> Result<()> {
|
||||
let file_path1 = Path::new("file1.txt");
|
||||
let file_path2 = Path::new("file2.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_stashed_untracked_and_modified() -> Result<()> {
|
||||
let file_path1 = Path::new("file1.txt");
|
||||
let file_path2 = Path::new("file2.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path1))?.write_all(b"test")?;
|
||||
stage_add_file(repo_path, file_path1)?;
|
||||
commit(repo_path, "c1")?;
|
||||
File::create(root.join(file_path1))?.write_all(b"test")?;
|
||||
stage_add_file(repo_path, file_path1)?;
|
||||
commit(repo_path, "c1")?;
|
||||
|
||||
File::create(&root.join(file_path1))?
|
||||
.write_all(b"modified")?;
|
||||
File::create(&root.join(file_path2))?.write_all(b"new")?;
|
||||
File::create(root.join(file_path1))?
|
||||
.write_all(b"modified")?;
|
||||
File::create(root.join(file_path2))?.write_all(b"new")?;
|
||||
|
||||
assert_eq!(get_statuses(repo_path), (2, 0));
|
||||
assert_eq!(get_statuses(repo_path), (2, 0));
|
||||
|
||||
let id = stash_save(repo_path, None, true, false)?;
|
||||
let id = stash_save(repo_path, None, true, false)?;
|
||||
|
||||
let diff = get_commit_files(repo_path, id)?;
|
||||
let diff = get_commit_files(repo_path, id, None)?;
|
||||
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff[0].status, StatusItemType::Modified);
|
||||
assert_eq!(diff[1].status, StatusItemType::New);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff[0].status, StatusItemType::Modified);
|
||||
assert_eq!(diff[1].status, StatusItemType::New);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
224
asyncgit/src/sync/commit_filter.rs
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
use super::{
|
||||
commit_details::get_author_of_commit,
|
||||
commit_files::get_commit_diff, CommitId,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use bitflags::bitflags;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use git2::{Diff, Repository};
|
||||
use std::sync::Arc;
|
||||
|
||||
///
|
||||
pub type SharedCommitFilterFn = Arc<
|
||||
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
|
||||
>;
|
||||
|
||||
///
|
||||
pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
|
||||
Arc::new(Box::new(
|
||||
move |repo: &Repository,
|
||||
commit_id: &CommitId|
|
||||
-> Result<bool> {
|
||||
let diff = get_commit_diff(
|
||||
repo,
|
||||
*commit_id,
|
||||
Some(file_path.clone()),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let contains_file = diff.deltas().len() > 0;
|
||||
|
||||
Ok(contains_file)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
///
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SearchFields: u32 {
|
||||
///
|
||||
const MESSAGE_SUMMARY = 1 << 0;
|
||||
///
|
||||
const MESSAGE_BODY = 1 << 1;
|
||||
///
|
||||
const FILENAMES = 1 << 2;
|
||||
///
|
||||
const AUTHORS = 1 << 3;
|
||||
//TODO:
|
||||
// const COMMIT_HASHES = 1 << 3;
|
||||
// ///
|
||||
// const DATES = 1 << 4;
|
||||
// ///
|
||||
// const DIFFS = 1 << 5;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SearchFields {
|
||||
fn default() -> Self {
|
||||
Self::MESSAGE_SUMMARY
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
///
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SearchOptions: u32 {
|
||||
///
|
||||
const CASE_SENSITIVE = 1 << 0;
|
||||
///
|
||||
const FUZZY_SEARCH = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SearchOptions {
|
||||
fn default() -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct LogFilterSearchOptions {
|
||||
///
|
||||
pub search_pattern: String,
|
||||
///
|
||||
pub fields: SearchFields,
|
||||
///
|
||||
pub options: SearchOptions,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub struct LogFilterSearch {
|
||||
///
|
||||
pub matcher: fuzzy_matcher::skim::SkimMatcherV2,
|
||||
///
|
||||
pub options: LogFilterSearchOptions,
|
||||
}
|
||||
|
||||
impl LogFilterSearch {
|
||||
///
|
||||
pub fn new(options: LogFilterSearchOptions) -> Self {
|
||||
let mut options = options;
|
||||
if !options.options.contains(SearchOptions::CASE_SENSITIVE) {
|
||||
options.search_pattern =
|
||||
options.search_pattern.to_lowercase();
|
||||
}
|
||||
Self {
|
||||
matcher: fuzzy_matcher::skim::SkimMatcherV2::default(),
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
fn match_diff(&self, diff: &Diff<'_>) -> bool {
|
||||
diff.deltas().any(|delta| {
|
||||
if delta
|
||||
.new_file()
|
||||
.path()
|
||||
.and_then(|file| file.as_os_str().to_str())
|
||||
.is_some_and(|file| self.match_text(file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
delta
|
||||
.old_file()
|
||||
.path()
|
||||
.and_then(|file| file.as_os_str().to_str())
|
||||
.is_some_and(|file| self.match_text(file))
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub fn match_text(&self, text: &str) -> bool {
|
||||
if self.options.options.contains(SearchOptions::FUZZY_SEARCH)
|
||||
{
|
||||
self.matcher
|
||||
.fuzzy_match(
|
||||
text,
|
||||
self.options.search_pattern.as_str(),
|
||||
)
|
||||
.is_some()
|
||||
} else if self
|
||||
.options
|
||||
.options
|
||||
.contains(SearchOptions::CASE_SENSITIVE)
|
||||
{
|
||||
text.contains(self.options.search_pattern.as_str())
|
||||
} else {
|
||||
text.to_lowercase()
|
||||
.contains(self.options.search_pattern.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn filter_commit_by_search(
|
||||
filter: LogFilterSearch,
|
||||
) -> SharedCommitFilterFn {
|
||||
Arc::new(Box::new(
|
||||
move |repo: &Repository,
|
||||
commit_id: &CommitId|
|
||||
-> Result<bool> {
|
||||
let mailmap = repo.mailmap()?;
|
||||
let commit = repo.find_commit((*commit_id).into())?;
|
||||
|
||||
let msg_summary_match = filter
|
||||
.options
|
||||
.fields
|
||||
.contains(SearchFields::MESSAGE_SUMMARY)
|
||||
.then(|| {
|
||||
commit.summary().map(|msg| filter.match_text(msg))
|
||||
})
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
let msg_body_match = filter
|
||||
.options
|
||||
.fields
|
||||
.contains(SearchFields::MESSAGE_BODY)
|
||||
.then(|| {
|
||||
commit.body().map(|msg| filter.match_text(msg))
|
||||
})
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
let file_match = filter
|
||||
.options
|
||||
.fields
|
||||
.contains(SearchFields::FILENAMES)
|
||||
.then(|| {
|
||||
get_commit_diff(
|
||||
repo, *commit_id, None, None, None,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.flatten()
|
||||
.is_some_and(|diff| filter.match_diff(&diff));
|
||||
|
||||
let authors_match = if filter
|
||||
.options
|
||||
.fields
|
||||
.contains(SearchFields::AUTHORS)
|
||||
{
|
||||
let author = get_author_of_commit(&commit, &mailmap);
|
||||
[author.email(), author.name()].iter().any(
|
||||
|opt_haystack| {
|
||||
opt_haystack.is_some_and(|haystack| {
|
||||
filter.match_text(haystack)
|
||||
})
|
||||
},
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
Ok(msg_summary_match
|
||||
|| msg_body_match
|
||||
|| file_match
|
||||
|| authors_match)
|
||||
},
|
||||
))
|
||||
}
|
||||
51
asyncgit/src/sync/commit_revert.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use super::{CommitId, RepoPath};
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{repository::repo, utils::read_file},
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
|
||||
const GIT_REVERT_HEAD_FILE: &str = "REVERT_HEAD";
|
||||
|
||||
///
|
||||
pub fn revert_commit(
|
||||
repo_path: &RepoPath,
|
||||
commit: CommitId,
|
||||
) -> Result<()> {
|
||||
scope_time!("revert");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let commit = repo.find_commit(commit.into())?;
|
||||
|
||||
repo.revert(&commit, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn revert_head(repo_path: &RepoPath) -> Result<CommitId> {
|
||||
scope_time!("revert_head");
|
||||
|
||||
let path = repo(repo_path)?.path().join(GIT_REVERT_HEAD_FILE);
|
||||
|
||||
let file_content = read_file(&path)?;
|
||||
|
||||
let id = git2::Oid::from_str(file_content.trim())?;
|
||||
|
||||
Ok(id.into())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn commit_revert(
|
||||
repo_path: &RepoPath,
|
||||
msg: &str,
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("commit_revert");
|
||||
|
||||
let id = crate::sync::commit(repo_path, msg)?;
|
||||
|
||||
repo(repo_path)?.cleanup_state()?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
|
@ -1,216 +1,349 @@
|
|||
use super::utils::repo;
|
||||
use crate::error::Result;
|
||||
use std::fmt::Display;
|
||||
|
||||
use super::RepoPath;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
commit_details::get_author_of_commit,
|
||||
repository::{gix_repo, repo},
|
||||
},
|
||||
};
|
||||
use git2::{Commit, Error, Oid};
|
||||
use scopetime::scope_time;
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
/// identifies a single commit
|
||||
#[derive(
|
||||
Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd,
|
||||
Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd,
|
||||
)]
|
||||
pub struct CommitId(Oid);
|
||||
|
||||
impl CommitId {
|
||||
/// create new `CommitId`
|
||||
pub const fn new(id: Oid) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) const fn get_oid(self) -> Oid {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// 7 chars short hash
|
||||
pub fn get_short_string(&self) -> String {
|
||||
self.to_string().chars().take(7).collect()
|
||||
}
|
||||
impl Default for CommitId {
|
||||
fn default() -> Self {
|
||||
Self(Oid::zero())
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: remove once clippy fixed: https://github.com/rust-lang/rust-clippy/issues/6983
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
impl ToString for CommitId {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
impl CommitId {
|
||||
/// create new `CommitId`
|
||||
pub const fn new(id: Oid) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
///
|
||||
pub(crate) const fn get_oid(self) -> Oid {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// 7 chars short hash
|
||||
pub fn get_short_string(&self) -> String {
|
||||
self.to_string().chars().take(7).collect()
|
||||
}
|
||||
|
||||
/// Tries to retrieve the `CommitId` form the revision if exists in the given repository
|
||||
pub fn from_revision(
|
||||
repo_path: &RepoPath,
|
||||
revision: &str,
|
||||
) -> Result<Self> {
|
||||
scope_time!("CommitId::from_revision");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let commit_obj = repo.revparse_single(revision)?;
|
||||
Ok(commit_obj.id().into())
|
||||
}
|
||||
|
||||
/// Tries to convert a &str representation of a commit id into
|
||||
/// a `CommitId`
|
||||
pub fn from_str_unchecked(commit_id_str: &str) -> Result<Self> {
|
||||
match Oid::from_str(commit_id_str) {
|
||||
Err(e) => Err(crate::Error::Generic(format!(
|
||||
"Could not convert {}",
|
||||
e.message()
|
||||
))),
|
||||
Ok(v) => Ok(Self::new(v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CommitId {
|
||||
fn fmt(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommitId> for Oid {
|
||||
fn from(id: CommitId) -> Self {
|
||||
id.0
|
||||
}
|
||||
fn from(id: CommitId) -> Self {
|
||||
id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for CommitId {
|
||||
fn from(id: Oid) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
fn from(id: Oid) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::ObjectId> for CommitId {
|
||||
fn from(object_id: gix::ObjectId) -> Self {
|
||||
#[allow(clippy::expect_used)]
|
||||
let oid = Oid::from_bytes(object_id.as_bytes()).expect("`Oid::from_bytes(object_id.as_bytes())` is expected to never fail");
|
||||
|
||||
Self::new(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gix::Commit<'_>> for CommitId {
|
||||
fn from(commit: gix::Commit<'_>) -> Self {
|
||||
#[allow(clippy::expect_used)]
|
||||
let oid = Oid::from_bytes(commit.id().as_bytes()).expect("`Oid::from_bytes(commit.id().as_bytes())` is expected to never fail");
|
||||
|
||||
Self::new(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommitId> for gix::ObjectId {
|
||||
fn from(id: CommitId) -> Self {
|
||||
Self::from_bytes_or_panic(id.0.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommitInfo {
|
||||
///
|
||||
pub message: String,
|
||||
///
|
||||
pub time: i64,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub id: CommitId,
|
||||
///
|
||||
pub message: String,
|
||||
///
|
||||
pub time: i64,
|
||||
///
|
||||
pub author: String,
|
||||
///
|
||||
pub id: CommitId,
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_commits_info(
|
||||
repo_path: &str,
|
||||
ids: &[CommitId],
|
||||
message_length_limit: usize,
|
||||
repo_path: &RepoPath,
|
||||
ids: &[CommitId],
|
||||
message_length_limit: usize,
|
||||
) -> Result<Vec<CommitInfo>> {
|
||||
scope_time!("get_commits_info");
|
||||
scope_time!("get_commits_info");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
let mailmap = repo.mailmap()?;
|
||||
|
||||
let commits = ids
|
||||
.iter()
|
||||
.map(|id| repo.find_commit((*id).into()))
|
||||
.collect::<std::result::Result<Vec<Commit>, Error>>()?
|
||||
.into_iter();
|
||||
let commits = ids
|
||||
.iter()
|
||||
.map(|id| repo.find_commit((*id).into()))
|
||||
.collect::<std::result::Result<Vec<Commit>, Error>>()?
|
||||
.into_iter();
|
||||
|
||||
let res = commits
|
||||
.map(|c: Commit| {
|
||||
let message = get_message(&c, Some(message_length_limit));
|
||||
let author = c.author().name().map_or_else(
|
||||
|| String::from("<unknown>"),
|
||||
String::from,
|
||||
);
|
||||
CommitInfo {
|
||||
message,
|
||||
author,
|
||||
time: c.time().seconds(),
|
||||
id: CommitId(c.id()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let res = commits
|
||||
.map(|c: Commit| {
|
||||
let message = get_message(&c, Some(message_length_limit));
|
||||
let author = get_author_of_commit(&c, &mailmap)
|
||||
.name()
|
||||
.map_or_else(
|
||||
|| String::from("<unknown>"),
|
||||
String::from,
|
||||
);
|
||||
CommitInfo {
|
||||
message,
|
||||
author,
|
||||
time: c.time().seconds(),
|
||||
id: CommitId(c.id()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(res)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_commit_info(
|
||||
repo_path: &str,
|
||||
commit_id: &CommitId,
|
||||
repo_path: &RepoPath,
|
||||
commit_id: &CommitId,
|
||||
) -> Result<CommitInfo> {
|
||||
scope_time!("get_commit_info");
|
||||
scope_time!("get_commit_info");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo: gix::Repository = gix_repo(repo_path)?;
|
||||
let mailmap = repo.open_mailmap();
|
||||
|
||||
let commit = repo.find_commit((*commit_id).into())?;
|
||||
let author = commit.author();
|
||||
let commit = repo.find_commit(*commit_id)?;
|
||||
let commit_ref = commit.decode()?;
|
||||
|
||||
Ok(CommitInfo {
|
||||
message: commit.message().unwrap_or("").into(),
|
||||
author: author.name().unwrap_or("<unknown>").into(),
|
||||
time: commit.time().seconds(),
|
||||
id: CommitId(commit.id()),
|
||||
})
|
||||
let message = gix_get_message(&commit_ref, None);
|
||||
|
||||
let author = commit_ref.author()?;
|
||||
|
||||
let author = mailmap.try_resolve(author).map_or_else(
|
||||
|| author.name.into(),
|
||||
|signature| signature.name,
|
||||
);
|
||||
|
||||
Ok(CommitInfo {
|
||||
message,
|
||||
author: author.to_string(),
|
||||
time: commit_ref.time()?.seconds,
|
||||
id: commit.id().detach().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// if `message_limit` is set the message will be
|
||||
/// limited to the first line and truncated to fit
|
||||
pub fn get_message(
|
||||
c: &Commit,
|
||||
message_limit: Option<usize>,
|
||||
c: &git2::Commit,
|
||||
message_limit: Option<usize>,
|
||||
) -> String {
|
||||
let msg = String::from_utf8_lossy(c.message_bytes());
|
||||
let msg = msg.trim();
|
||||
let msg = String::from_utf8_lossy(c.message_bytes());
|
||||
let msg = msg.trim();
|
||||
|
||||
message_limit.map_or_else(
|
||||
|| msg.to_string(),
|
||||
|limit| {
|
||||
let msg = msg.lines().next().unwrap_or_default();
|
||||
msg.unicode_truncate(limit).0.to_string()
|
||||
},
|
||||
)
|
||||
message_limit.map_or_else(
|
||||
|| msg.to_string(),
|
||||
|limit| {
|
||||
let msg = msg.lines().next().unwrap_or_default();
|
||||
msg.unicode_truncate(limit).0.to_string()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// if `message_limit` is set the message will be
|
||||
/// limited to the first line and truncated to fit
|
||||
pub fn gix_get_message(
|
||||
commit_ref: &gix::objs::CommitRef,
|
||||
message_limit: Option<usize>,
|
||||
) -> String {
|
||||
let message = commit_ref.message.to_string();
|
||||
let message = message.trim();
|
||||
|
||||
message_limit.map_or_else(
|
||||
|| message.to_string(),
|
||||
|limit| {
|
||||
let message = message.lines().next().unwrap_or_default();
|
||||
message.unicode_truncate(limit).0.to_string()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::get_commits_info;
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty,
|
||||
utils::get_head_repo,
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use super::get_commits_info;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
commit, stage_add_file, tests::repo_init_empty,
|
||||
utils::get_head_repo, CommitId, RepoPath,
|
||||
},
|
||||
};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_log() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_log() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c1 = commit(repo_path, "commit1").unwrap();
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c2 = commit(repo_path, "commit2").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c1 = commit(repo_path, "commit1").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c2 = commit(repo_path, "commit2").unwrap();
|
||||
|
||||
let res =
|
||||
get_commits_info(repo_path, &vec![c2, c1], 50).unwrap();
|
||||
let res = get_commits_info(repo_path, &[c2, c1], 50).unwrap();
|
||||
|
||||
assert_eq!(res.len(), 2);
|
||||
assert_eq!(res[0].message.as_str(), "commit2");
|
||||
assert_eq!(res[0].author.as_str(), "name");
|
||||
assert_eq!(res[1].message.as_str(), "commit1");
|
||||
assert_eq!(res.len(), 2);
|
||||
assert_eq!(res[0].message.as_str(), "commit2");
|
||||
assert_eq!(res[0].author.as_str(), "name");
|
||||
assert_eq!(res[1].message.as_str(), "commit1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File::create(root.join(".mailmap"))?
|
||||
.write_all(b"new name <newemail> <email>")?;
|
||||
let res = get_commits_info(repo_path, &[c2], 50).unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_log_first_msg_line() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
assert_eq!(res[0].author.as_str(), "new name");
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c1 = commit(repo_path, "subject\nbody").unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let res = get_commits_info(repo_path, &vec![c1], 50).unwrap();
|
||||
#[test]
|
||||
fn test_log_first_msg_line() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].message.as_str(), "subject");
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let c1 = commit(repo_path, "subject\nbody").unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let res = get_commits_info(repo_path, &[c1], 50).unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
assert_eq!(res.len(), 1);
|
||||
assert_eq!(res[0].message.as_str(), "subject");
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let msg = invalidstring::invalid_utf8("test msg");
|
||||
commit(repo_path, msg.as_str()).unwrap();
|
||||
#[test]
|
||||
fn test_invalid_utf8() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let res = get_commits_info(
|
||||
repo_path,
|
||||
&vec![get_head_repo(&repo).unwrap().into()],
|
||||
50,
|
||||
)
|
||||
.unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
|
||||
assert_eq!(res.len(), 1);
|
||||
dbg!(&res[0].message);
|
||||
assert_eq!(res[0].message.starts_with("test msg"), true);
|
||||
let msg = invalidstring::invalid_utf8("test msg");
|
||||
commit(repo_path, msg.as_str()).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let res = get_commits_info(
|
||||
repo_path,
|
||||
&[get_head_repo(&repo).unwrap()],
|
||||
50,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res.len(), 1);
|
||||
dbg!(&res[0].message);
|
||||
assert!(res[0].message.starts_with("test msg"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_commit_from_revision() -> Result<()> {
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let foo_file = Path::new("foo");
|
||||
File::create(root.join(foo_file))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, foo_file).unwrap();
|
||||
let c1 = commit(repo_path, "subject: foo\nbody").unwrap();
|
||||
let c1_rev = c1.get_short_string();
|
||||
|
||||
assert_eq!(
|
||||
CommitId::from_revision(repo_path, c1_rev.as_str())
|
||||
.unwrap(),
|
||||
c1
|
||||
);
|
||||
|
||||
const FOREIGN_HASH: &str =
|
||||
"d6d7d55cb6e4ba7301d6a11a657aab4211e5777e";
|
||||
assert!(
|
||||
CommitId::from_revision(repo_path, FOREIGN_HASH).is_err()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,114 +1,170 @@
|
|||
use super::utils::repo;
|
||||
use crate::error::Result;
|
||||
use git2::Repository;
|
||||
use scopetime::scope_time;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{repository::repo, RepoPath};
|
||||
|
||||
// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles
|
||||
/// represents the `status.showUntrackedFiles` git config state
|
||||
#[derive(
|
||||
Hash, Copy, Clone, Default, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum ShowUntrackedFilesConfig {
|
||||
///
|
||||
No,
|
||||
///
|
||||
Normal,
|
||||
///
|
||||
All,
|
||||
///
|
||||
#[default]
|
||||
No,
|
||||
///
|
||||
Normal,
|
||||
///
|
||||
All,
|
||||
}
|
||||
|
||||
impl ShowUntrackedFilesConfig {
|
||||
///
|
||||
pub const fn include_none(&self) -> bool {
|
||||
matches!(self, Self::No)
|
||||
}
|
||||
///
|
||||
pub const fn include_none(self) -> bool {
|
||||
matches!(self, Self::No)
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn include_untracked(&self) -> bool {
|
||||
matches!(self, Self::Normal | Self::All)
|
||||
}
|
||||
///
|
||||
pub const fn include_untracked(self) -> bool {
|
||||
matches!(self, Self::Normal | Self::All)
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn recurse_untracked_dirs(&self) -> bool {
|
||||
matches!(self, Self::All)
|
||||
}
|
||||
///
|
||||
pub const fn recurse_untracked_dirs(self) -> bool {
|
||||
matches!(self, Self::All)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn untracked_files_config_repo(
|
||||
repo: &Repository,
|
||||
repo: &Repository,
|
||||
) -> Result<ShowUntrackedFilesConfig> {
|
||||
let show_untracked_files =
|
||||
get_config_string_repo(repo, "status.showUntrackedFiles")?;
|
||||
let show_untracked_files =
|
||||
get_config_string_repo(repo, "status.showUntrackedFiles")?;
|
||||
|
||||
if let Some(show_untracked_files) = show_untracked_files {
|
||||
if &show_untracked_files == "no" {
|
||||
return Ok(ShowUntrackedFilesConfig::No);
|
||||
} else if &show_untracked_files == "normal" {
|
||||
return Ok(ShowUntrackedFilesConfig::Normal);
|
||||
}
|
||||
}
|
||||
if let Some(show_untracked_files) = show_untracked_files {
|
||||
if &show_untracked_files == "no" {
|
||||
return Ok(ShowUntrackedFilesConfig::No);
|
||||
} else if &show_untracked_files == "normal" {
|
||||
return Ok(ShowUntrackedFilesConfig::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ShowUntrackedFilesConfig::All)
|
||||
// This does not reflect how git works according to its docs that say: "If this variable is not
|
||||
// specified, it defaults to `normal`."
|
||||
//
|
||||
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-statusshowUntrackedFiles
|
||||
//
|
||||
// Note that this might become less relevant over time as more code gets migrated to `gitoxide`
|
||||
// because `gitoxide` respects `status.showUntrackedFiles` by default.
|
||||
Ok(ShowUntrackedFilesConfig::All)
|
||||
}
|
||||
|
||||
// see https://git-scm.com/docs/git-config#Documentation/git-config.txt-pushdefault
|
||||
/// represents `push.default` git config
|
||||
#[derive(PartialEq, Default, Eq)]
|
||||
pub enum PushDefaultStrategyConfig {
|
||||
Nothing,
|
||||
Current,
|
||||
Upstream,
|
||||
#[default]
|
||||
Simple,
|
||||
Matching,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for PushDefaultStrategyConfig {
|
||||
type Error = crate::Error;
|
||||
fn try_from(
|
||||
value: &'a str,
|
||||
) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
"nothing" => Ok(Self::Nothing),
|
||||
"current" => Ok(Self::Current),
|
||||
"upstream" | "tracking" => Ok(Self::Upstream),
|
||||
"simple" => Ok(Self::Simple),
|
||||
"matching" => Ok(Self::Matching),
|
||||
_ => Err(crate::Error::GitConfig(format!(
|
||||
"malformed value for push.default: {value}, must be one of nothing, matching, simple, upstream or current"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_default_strategy_config_repo(
|
||||
repo: &Repository,
|
||||
) -> Result<PushDefaultStrategyConfig> {
|
||||
(get_config_string_repo(repo, "push.default")?).map_or_else(
|
||||
|| Ok(PushDefaultStrategyConfig::default()),
|
||||
|entry_str| {
|
||||
PushDefaultStrategyConfig::try_from(entry_str.as_str())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn untracked_files_config(
|
||||
repo_path: &str,
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<ShowUntrackedFilesConfig> {
|
||||
let repo = repo(repo_path)?;
|
||||
untracked_files_config_repo(&repo)
|
||||
let repo = repo(repo_path)?;
|
||||
untracked_files_config_repo(&repo)
|
||||
}
|
||||
|
||||
/// get string from config
|
||||
pub fn get_config_string(
|
||||
repo_path: &str,
|
||||
key: &str,
|
||||
repo_path: &RepoPath,
|
||||
key: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let repo = repo(repo_path)?;
|
||||
get_config_string_repo(&repo, key)
|
||||
let repo = repo(repo_path)?;
|
||||
get_config_string_repo(&repo, key)
|
||||
}
|
||||
|
||||
pub fn get_config_string_repo(
|
||||
repo: &Repository,
|
||||
key: &str,
|
||||
repo: &Repository,
|
||||
key: &str,
|
||||
) -> Result<Option<String>> {
|
||||
scope_time!("get_config_string_repo");
|
||||
scope_time!("get_config_string_repo");
|
||||
|
||||
let cfg = repo.config()?;
|
||||
let cfg = repo.config()?;
|
||||
|
||||
// this code doesnt match what the doc says regarding what
|
||||
// gets returned when but it actually works
|
||||
let entry_res = cfg.get_entry(key);
|
||||
// this code doesn't match what the doc says regarding what
|
||||
// gets returned when but it actually works
|
||||
let entry_res = cfg.get_entry(key);
|
||||
|
||||
let entry = match entry_res {
|
||||
Ok(ent) => ent,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
let Ok(entry) = entry_res else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if entry.has_value() {
|
||||
Ok(entry.value().map(std::string::ToString::to_string))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
if entry.has_value() {
|
||||
Ok(entry.value().map(std::string::ToString::to_string))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::tests::repo_init;
|
||||
use super::*;
|
||||
use crate::sync::tests::repo_init;
|
||||
|
||||
#[test]
|
||||
fn test_get_config() {
|
||||
let bad_dir_cfg =
|
||||
get_config_string("oodly_noodly", "this.doesnt.exist");
|
||||
assert!(bad_dir_cfg.is_err());
|
||||
#[test]
|
||||
fn test_get_config() {
|
||||
let bad_dir_cfg = get_config_string(
|
||||
&"oodly_noodly".into(),
|
||||
"this.doesnt.exist",
|
||||
);
|
||||
assert!(bad_dir_cfg.is_err());
|
||||
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let path = repo.path();
|
||||
let rpath = path.as_os_str().to_str().unwrap();
|
||||
let bad_cfg = get_config_string(rpath, "this.doesnt.exist");
|
||||
assert!(bad_cfg.is_ok());
|
||||
assert!(bad_cfg.unwrap().is_none());
|
||||
// repo init sets user.name
|
||||
let good_cfg = get_config_string(rpath, "user.name");
|
||||
assert!(good_cfg.is_ok());
|
||||
assert!(good_cfg.unwrap().is_some());
|
||||
}
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let path = repo.path();
|
||||
let rpath = path.as_os_str().to_str().unwrap();
|
||||
let bad_cfg =
|
||||
get_config_string(&rpath.into(), "this.doesnt.exist");
|
||||
assert!(bad_cfg.is_ok());
|
||||
assert!(bad_cfg.unwrap().is_none());
|
||||
// repo init sets user.name
|
||||
let good_cfg = get_config_string(&rpath.into(), "user.name");
|
||||
assert!(good_cfg.is_ok());
|
||||
assert!(good_cfg.unwrap().is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,253 +1,367 @@
|
|||
//! credentials git helper
|
||||
|
||||
use super::remotes::get_default_remote_in_repo;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
CWD,
|
||||
use super::{
|
||||
remotes::{
|
||||
get_default_remote_for_fetch_in_repo,
|
||||
get_default_remote_for_push_in_repo,
|
||||
get_default_remote_in_repo,
|
||||
},
|
||||
repository::repo,
|
||||
RepoPath,
|
||||
};
|
||||
use git2::{Config, CredentialHelper};
|
||||
use crate::error::{Error, Result};
|
||||
use git2::CredentialHelper;
|
||||
|
||||
/// basic Authentication Credentials
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct BasicAuthCredential {
|
||||
///
|
||||
pub username: Option<String>,
|
||||
///
|
||||
pub password: Option<String>,
|
||||
///
|
||||
pub username: Option<String>,
|
||||
///
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl BasicAuthCredential {
|
||||
///
|
||||
pub const fn is_complete(&self) -> bool {
|
||||
self.username.is_some() && self.password.is_some()
|
||||
}
|
||||
///
|
||||
pub const fn new(
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Self {
|
||||
Self { username, password }
|
||||
}
|
||||
///
|
||||
pub const fn is_complete(&self) -> bool {
|
||||
self.username.is_some() && self.password.is_some()
|
||||
}
|
||||
///
|
||||
pub const fn new(
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Self {
|
||||
Self { username, password }
|
||||
}
|
||||
}
|
||||
|
||||
/// know if username and password are needed for this url
|
||||
pub fn need_username_password() -> Result<bool> {
|
||||
let repo = crate::sync::utils::repo(CWD)?;
|
||||
let url = repo
|
||||
.find_remote(&get_default_remote_in_repo(&repo)?)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let is_http = url.starts_with("http");
|
||||
Ok(is_http)
|
||||
pub fn need_username_password(repo_path: &RepoPath) -> Result<bool> {
|
||||
let repo = repo(repo_path)?;
|
||||
let remote =
|
||||
repo.find_remote(&get_default_remote_in_repo(&repo)?)?;
|
||||
let url = remote
|
||||
.pushurl()
|
||||
.or_else(|| remote.url())
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let is_http = url.starts_with("http");
|
||||
Ok(is_http)
|
||||
}
|
||||
|
||||
/// know if username and password are needed for this url
|
||||
/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also
|
||||
/// `need_username_password`.
|
||||
pub fn need_username_password_for_fetch(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<bool> {
|
||||
let repo = repo(repo_path)?;
|
||||
let remote = repo
|
||||
.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?;
|
||||
let url = remote
|
||||
.url()
|
||||
.or_else(|| remote.url())
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let is_http = url.starts_with("http");
|
||||
Ok(is_http)
|
||||
}
|
||||
|
||||
/// know if username and password are needed for this url
|
||||
/// TODO: Very similar to `need_username_password_for_fetch`. Can be refactored. See also
|
||||
/// `need_username_password`.
|
||||
pub fn need_username_password_for_push(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<bool> {
|
||||
let repo = repo(repo_path)?;
|
||||
let remote = repo
|
||||
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?;
|
||||
let url = remote
|
||||
.pushurl()
|
||||
.or_else(|| remote.url())
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let is_http = url.starts_with("http");
|
||||
Ok(is_http)
|
||||
}
|
||||
|
||||
/// extract username and password
|
||||
pub fn extract_username_password() -> Result<BasicAuthCredential> {
|
||||
let repo = crate::sync::utils::repo(CWD)?;
|
||||
let url = repo
|
||||
.find_remote(&get_default_remote_in_repo(&repo)?)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let mut helper = CredentialHelper::new(&url);
|
||||
pub fn extract_username_password(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<BasicAuthCredential> {
|
||||
let repo = repo(repo_path)?;
|
||||
let url = repo
|
||||
.find_remote(&get_default_remote_in_repo(&repo)?)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let mut helper = CredentialHelper::new(&url);
|
||||
|
||||
if let Ok(config) = Config::open_default() {
|
||||
helper.config(&config);
|
||||
}
|
||||
Ok(match helper.execute() {
|
||||
Some((username, password)) => {
|
||||
BasicAuthCredential::new(Some(username), Some(password))
|
||||
}
|
||||
None => extract_cred_from_url(&url),
|
||||
})
|
||||
//TODO: look at Cred::credential_helper,
|
||||
//if the username is in the url we need to set it here,
|
||||
//I dont think `config` will pick it up
|
||||
|
||||
if let Ok(config) = repo.config() {
|
||||
helper.config(&config);
|
||||
}
|
||||
|
||||
Ok(match helper.execute() {
|
||||
Some((username, password)) => {
|
||||
BasicAuthCredential::new(Some(username), Some(password))
|
||||
}
|
||||
None => extract_cred_from_url(&url),
|
||||
})
|
||||
}
|
||||
|
||||
/// extract username and password
|
||||
/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.
|
||||
pub fn extract_username_password_for_fetch(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<BasicAuthCredential> {
|
||||
let repo = repo(repo_path)?;
|
||||
let url = repo
|
||||
.find_remote(&get_default_remote_for_fetch_in_repo(&repo)?)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let mut helper = CredentialHelper::new(&url);
|
||||
|
||||
//TODO: look at Cred::credential_helper,
|
||||
//if the username is in the url we need to set it here,
|
||||
//I dont think `config` will pick it up
|
||||
|
||||
if let Ok(config) = repo.config() {
|
||||
helper.config(&config);
|
||||
}
|
||||
|
||||
Ok(match helper.execute() {
|
||||
Some((username, password)) => {
|
||||
BasicAuthCredential::new(Some(username), Some(password))
|
||||
}
|
||||
None => extract_cred_from_url(&url),
|
||||
})
|
||||
}
|
||||
|
||||
/// extract username and password
|
||||
/// TODO: Very similar to `extract_username_password_for_fetch`. Can be refactored.
|
||||
pub fn extract_username_password_for_push(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<BasicAuthCredential> {
|
||||
let repo = repo(repo_path)?;
|
||||
let url = repo
|
||||
.find_remote(&get_default_remote_for_push_in_repo(&repo)?)?
|
||||
.url()
|
||||
.ok_or(Error::UnknownRemote)?
|
||||
.to_owned();
|
||||
let mut helper = CredentialHelper::new(&url);
|
||||
|
||||
//TODO: look at Cred::credential_helper,
|
||||
//if the username is in the url we need to set it here,
|
||||
//I dont think `config` will pick it up
|
||||
|
||||
if let Ok(config) = repo.config() {
|
||||
helper.config(&config);
|
||||
}
|
||||
|
||||
Ok(match helper.execute() {
|
||||
Some((username, password)) => {
|
||||
BasicAuthCredential::new(Some(username), Some(password))
|
||||
}
|
||||
None => extract_cred_from_url(&url),
|
||||
})
|
||||
}
|
||||
|
||||
/// extract credentials from url
|
||||
pub fn extract_cred_from_url(url: &str) -> BasicAuthCredential {
|
||||
if let Ok(url) = url::Url::parse(url) {
|
||||
BasicAuthCredential::new(
|
||||
if url.username() == "" {
|
||||
None
|
||||
} else {
|
||||
Some(url.username().to_owned())
|
||||
},
|
||||
url.password().map(std::borrow::ToOwned::to_owned),
|
||||
)
|
||||
} else {
|
||||
BasicAuthCredential::new(None, None)
|
||||
}
|
||||
url::Url::parse(url).map_or_else(
|
||||
|_| BasicAuthCredential::new(None, None),
|
||||
|url| {
|
||||
BasicAuthCredential::new(
|
||||
if url.username() == "" {
|
||||
None
|
||||
} else {
|
||||
Some(url.username().to_owned())
|
||||
},
|
||||
url.password().map(std::borrow::ToOwned::to_owned),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::sync::{
|
||||
cred::{
|
||||
extract_cred_from_url, extract_username_password,
|
||||
need_username_password, BasicAuthCredential,
|
||||
},
|
||||
remotes::DEFAULT_REMOTE_NAME,
|
||||
tests::repo_init,
|
||||
};
|
||||
use serial_test::serial;
|
||||
use std::env;
|
||||
use crate::sync::{
|
||||
cred::{
|
||||
extract_cred_from_url, extract_username_password,
|
||||
need_username_password, BasicAuthCredential,
|
||||
},
|
||||
remotes::DEFAULT_REMOTE_NAME,
|
||||
tests::repo_init,
|
||||
RepoPath,
|
||||
};
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
fn test_credential_complete() {
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete(),
|
||||
true
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_credential_complete() {
|
||||
assert!(BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credential_not_complete() {
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
None,
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete(),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
None
|
||||
)
|
||||
.is_complete(),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
BasicAuthCredential::new(None, None).is_complete(),
|
||||
false
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_credential_not_complete() {
|
||||
assert!(!BasicAuthCredential::new(
|
||||
None,
|
||||
Some("password".to_owned())
|
||||
)
|
||||
.is_complete());
|
||||
assert!(!BasicAuthCredential::new(
|
||||
Some("username".to_owned()),
|
||||
None
|
||||
)
|
||||
.is_complete());
|
||||
assert!(!BasicAuthCredential::new(None, None).is_complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_username_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user@github.com"),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_extract_username_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user@github.com"),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_username_password_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user:pwd@github.com"),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pwd".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_extract_username_password_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://user:pwd@github.com"),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pwd".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_nothing_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://github.com"),
|
||||
BasicAuthCredential::new(None, None)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_extract_nothing_from_url() {
|
||||
assert_eq!(
|
||||
extract_cred_from_url("https://github.com"),
|
||||
BasicAuthCredential::new(None, None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_need_username_password_if_https() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_need_username_password_if_https() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(need_username_password().unwrap(), true);
|
||||
}
|
||||
assert!(need_username_password(repo_path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_dont_need_username_password_if_ssh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_dont_need_username_password_if_ssh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
|
||||
.unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "git@github.com:user/repo")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(need_username_password().unwrap(), false);
|
||||
}
|
||||
assert!(!need_username_password(repo_path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_dont_need_username_password_if_pushurl_ssh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
repo.remote_set_pushurl(
|
||||
DEFAULT_REMOTE_NAME,
|
||||
Some("git@github.com:user/repo"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
need_username_password().unwrap();
|
||||
}
|
||||
assert!(!need_username_password(repo_path).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_password_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_retrieve_if_need_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(
|
||||
DEFAULT_REMOTE_NAME,
|
||||
"http://user:pass@github.com",
|
||||
)
|
||||
.unwrap();
|
||||
need_username_password(repo_path).unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
extract_username_password().unwrap(),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pass".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_password_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
repo.remote(
|
||||
DEFAULT_REMOTE_NAME,
|
||||
"http://user:pass@github.com",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
extract_username_password(repo_path).unwrap(),
|
||||
BasicAuthCredential::new(
|
||||
Some("user".to_owned()),
|
||||
Some("pass".to_owned())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
extract_username_password().unwrap(),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_extract_username_from_repo() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_extract_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
repo.remote(DEFAULT_REMOTE_NAME, "http://user@github.com")
|
||||
.unwrap();
|
||||
|
||||
env::set_current_dir(repo_path).unwrap();
|
||||
assert_eq!(
|
||||
extract_username_password(repo_path).unwrap(),
|
||||
BasicAuthCredential::new(Some("user".to_owned()), None)
|
||||
);
|
||||
}
|
||||
|
||||
extract_username_password().unwrap();
|
||||
}
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic]
|
||||
fn test_error_if_no_remote_when_trying_to_extract_username_password(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
extract_username_password(repo_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,389 +1,501 @@
|
|||
use super::utils::{repo, work_dir};
|
||||
use crate::error::{Error, Result};
|
||||
use scopetime::scope_time;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
process::Command,
|
||||
use super::{repository::repo, RepoPath};
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{
|
||||
branch::get_branch_upstream_merge,
|
||||
config::{
|
||||
push_default_strategy_config_repo,
|
||||
PushDefaultStrategyConfig,
|
||||
},
|
||||
remotes::{proxy_auto, tags::tags_missing_remote, Callbacks},
|
||||
},
|
||||
};
|
||||
|
||||
const HOOK_POST_COMMIT: &str = ".git/hooks/post-commit";
|
||||
const HOOK_PRE_COMMIT: &str = ".git/hooks/pre-commit";
|
||||
const HOOK_COMMIT_MSG: &str = ".git/hooks/commit-msg";
|
||||
const HOOK_COMMIT_MSG_TEMP_FILE: &str = ".git/COMMIT_EDITMSG";
|
||||
|
||||
/// this hook is documented here <https://git-scm.com/docs/githooks#_commit_msg>
|
||||
/// we use the same convention as other git clients to create a temp file containing
|
||||
/// the commit message at `.git/COMMIT_EDITMSG` and pass it's relative path as the only
|
||||
/// parameter to the hook script.
|
||||
pub fn hooks_commit_msg(
|
||||
repo_path: &str,
|
||||
msg: &mut String,
|
||||
) -> Result<HookResult> {
|
||||
scope_time!("hooks_commit_msg");
|
||||
|
||||
let work_dir = work_dir_as_string(repo_path)?;
|
||||
|
||||
if hook_runable(work_dir.as_str(), HOOK_COMMIT_MSG) {
|
||||
let temp_file = Path::new(work_dir.as_str())
|
||||
.join(HOOK_COMMIT_MSG_TEMP_FILE);
|
||||
File::create(&temp_file)?.write_all(msg.as_bytes())?;
|
||||
|
||||
let res = run_hook(
|
||||
work_dir.as_str(),
|
||||
HOOK_COMMIT_MSG,
|
||||
&[HOOK_COMMIT_MSG_TEMP_FILE],
|
||||
)?;
|
||||
|
||||
// load possibly altered msg
|
||||
msg.clear();
|
||||
File::open(temp_file)?.read_to_string(msg)?;
|
||||
|
||||
Ok(res)
|
||||
} else {
|
||||
Ok(HookResult::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
/// this hook is documented here <https://git-scm.com/docs/githooks#_pre_commit>
|
||||
///
|
||||
pub fn hooks_pre_commit(repo_path: &str) -> Result<HookResult> {
|
||||
scope_time!("hooks_pre_commit");
|
||||
|
||||
let work_dir = work_dir_as_string(repo_path)?;
|
||||
|
||||
if hook_runable(work_dir.as_str(), HOOK_PRE_COMMIT) {
|
||||
Ok(run_hook(work_dir.as_str(), HOOK_PRE_COMMIT, &[])?)
|
||||
} else {
|
||||
Ok(HookResult::Ok)
|
||||
}
|
||||
}
|
||||
///
|
||||
pub fn hooks_post_commit(repo_path: &str) -> Result<HookResult> {
|
||||
scope_time!("hooks_post_commit");
|
||||
|
||||
let work_dir = work_dir_as_string(repo_path)?;
|
||||
let work_dir_str = work_dir.as_str();
|
||||
|
||||
if hook_runable(work_dir_str, HOOK_POST_COMMIT) {
|
||||
Ok(run_hook(work_dir_str, HOOK_POST_COMMIT, &[])?)
|
||||
} else {
|
||||
Ok(HookResult::Ok)
|
||||
}
|
||||
}
|
||||
|
||||
fn work_dir_as_string(repo_path: &str) -> Result<String> {
|
||||
let repo = repo(repo_path)?;
|
||||
work_dir(&repo)?
|
||||
.to_str()
|
||||
.map(std::string::ToString::to_string)
|
||||
.ok_or_else(|| {
|
||||
Error::Generic(
|
||||
"workdir contains invalid utf8".to_string(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn hook_runable(path: &str, hook: &str) -> bool {
|
||||
let path = Path::new(path);
|
||||
let path = path.join(hook);
|
||||
|
||||
path.exists() && is_executable(&path)
|
||||
}
|
||||
use git2::{BranchType, Direction, Oid};
|
||||
pub use git2_hooks::{PrePushRef, PrepareCommitMsgSource};
|
||||
use scopetime::scope_time;
|
||||
use std::collections::HashMap;
|
||||
|
||||
///
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum HookResult {
|
||||
/// Everything went fine
|
||||
Ok,
|
||||
/// Hook returned error
|
||||
NotOk(String),
|
||||
/// Everything went fine
|
||||
Ok,
|
||||
/// Hook returned error
|
||||
NotOk(String),
|
||||
}
|
||||
|
||||
/// this function calls hook scripts based on conventions documented here
|
||||
/// see <https://git-scm.com/docs/githooks>
|
||||
fn run_hook(
|
||||
path: &str,
|
||||
hook_script: &str,
|
||||
args: &[&str],
|
||||
impl From<git2_hooks::HookResult> for HookResult {
|
||||
fn from(v: git2_hooks::HookResult) -> Self {
|
||||
match v {
|
||||
git2_hooks::HookResult::NoHookFound => Self::Ok,
|
||||
git2_hooks::HookResult::Run(response) => {
|
||||
if response.is_successful() {
|
||||
Self::Ok
|
||||
} else {
|
||||
Self::NotOk(if response.stderr.is_empty() {
|
||||
response.stdout
|
||||
} else if response.stdout.is_empty() {
|
||||
response.stderr
|
||||
} else {
|
||||
format!(
|
||||
"{}\n{}",
|
||||
response.stdout, response.stderr
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve advertised refs from the remote for the upcoming push.
|
||||
fn advertised_remote_refs(
|
||||
repo_path: &RepoPath,
|
||||
remote: Option<&str>,
|
||||
url: &str,
|
||||
basic_credential: Option<crate::sync::cred::BasicAuthCredential>,
|
||||
) -> Result<HashMap<String, Oid>> {
|
||||
let repo = repo(repo_path)?;
|
||||
let mut remote_handle = if let Some(name) = remote {
|
||||
repo.find_remote(name)?
|
||||
} else {
|
||||
repo.remote_anonymous(url)?
|
||||
};
|
||||
|
||||
let callbacks = Callbacks::new(None, basic_credential);
|
||||
let conn = remote_handle.connect_auth(
|
||||
Direction::Push,
|
||||
Some(callbacks.callbacks()),
|
||||
Some(proxy_auto()),
|
||||
)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
for head in conn.list()? {
|
||||
map.insert(head.name().to_string(), head.oid());
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// Determine the remote ref name for a branch push.
|
||||
///
|
||||
/// Respects `push.default=upstream` config when set and upstream is configured.
|
||||
/// Otherwise defaults to `refs/heads/{branch}`. Delete operations always use
|
||||
/// the simple ref name.
|
||||
fn get_remote_ref_for_push(
|
||||
repo_path: &RepoPath,
|
||||
branch: &str,
|
||||
delete: bool,
|
||||
) -> Result<String> {
|
||||
// For delete operations, always use the simple ref name
|
||||
// regardless of push.default configuration
|
||||
if delete {
|
||||
return Ok(format!("refs/heads/{branch}"));
|
||||
}
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let push_default_strategy =
|
||||
push_default_strategy_config_repo(&repo)?;
|
||||
|
||||
// When push.default=upstream, use the configured upstream ref if available
|
||||
if push_default_strategy == PushDefaultStrategyConfig::Upstream {
|
||||
if let Ok(Some(upstream_ref)) =
|
||||
get_branch_upstream_merge(repo_path, branch)
|
||||
{
|
||||
return Ok(upstream_ref);
|
||||
}
|
||||
// If upstream strategy is set but no upstream is configured,
|
||||
// fall through to default behavior
|
||||
}
|
||||
|
||||
// Default: push to remote branch with same name as local
|
||||
Ok(format!("refs/heads/{branch}"))
|
||||
}
|
||||
|
||||
/// see `git2_hooks::hooks_commit_msg`
|
||||
pub fn hooks_commit_msg(
|
||||
repo_path: &RepoPath,
|
||||
msg: &mut String,
|
||||
) -> Result<HookResult> {
|
||||
let arg_str = format!("{} {}", hook_script, args.join(" "));
|
||||
let bash_args = vec!["-c".to_string(), arg_str];
|
||||
scope_time!("hooks_commit_msg");
|
||||
|
||||
let output = Command::new("bash")
|
||||
.args(bash_args)
|
||||
.current_dir(path)
|
||||
// This call forces Command to handle the Path environment correctly on windows,
|
||||
// the specific env set here does not matter
|
||||
// see https://github.com/rust-lang/rust/issues/37519
|
||||
.env(
|
||||
"DUMMY_ENV_TO_FIX_WINDOWS_CMD_RUNS",
|
||||
"FixPathHandlingOnWindows",
|
||||
)
|
||||
.output()?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(HookResult::Ok)
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&output.stderr);
|
||||
let out = String::from_utf8_lossy(&output.stdout);
|
||||
let formatted = format!("{}{}", out, err);
|
||||
|
||||
Ok(HookResult::NotOk(formatted))
|
||||
}
|
||||
Ok(git2_hooks::hooks_commit_msg(&repo, None, msg)?.into())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn is_executable(path: &Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let metadata = match path.metadata() {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => return false,
|
||||
};
|
||||
/// see `git2_hooks::hooks_pre_commit`
|
||||
pub fn hooks_pre_commit(repo_path: &RepoPath) -> Result<HookResult> {
|
||||
scope_time!("hooks_pre_commit");
|
||||
|
||||
let permissions = metadata.permissions();
|
||||
permissions.mode() & 0o111 != 0
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
Ok(git2_hooks::hooks_pre_commit(&repo, None)?.into())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
/// windows does not consider bash scripts to be executable so we consider everything
|
||||
/// to be executable (which is not far from the truth for windows platform.)
|
||||
const fn is_executable(_: &Path) -> bool {
|
||||
true
|
||||
/// see `git2_hooks::hooks_post_commit`
|
||||
pub fn hooks_post_commit(repo_path: &RepoPath) -> Result<HookResult> {
|
||||
scope_time!("hooks_post_commit");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
Ok(git2_hooks::hooks_post_commit(&repo, None)?.into())
|
||||
}
|
||||
|
||||
/// see `git2_hooks::hooks_prepare_commit_msg`
|
||||
pub fn hooks_prepare_commit_msg(
|
||||
repo_path: &RepoPath,
|
||||
source: PrepareCommitMsgSource,
|
||||
msg: &mut String,
|
||||
) -> Result<HookResult> {
|
||||
scope_time!("hooks_prepare_commit_msg");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
Ok(git2_hooks::hooks_prepare_commit_msg(
|
||||
&repo, None, source, msg,
|
||||
)?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// see `git2_hooks::hooks_pre_push`
|
||||
pub fn hooks_pre_push(
|
||||
repo_path: &RepoPath,
|
||||
remote: &str,
|
||||
push: &PrePushTarget<'_>,
|
||||
basic_credential: Option<crate::sync::cred::BasicAuthCredential>,
|
||||
) -> Result<HookResult> {
|
||||
scope_time!("hooks_pre_push");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
if !git2_hooks::hook_available(
|
||||
&repo,
|
||||
None,
|
||||
git2_hooks::HOOK_PRE_PUSH,
|
||||
)? {
|
||||
return Ok(HookResult::Ok);
|
||||
}
|
||||
|
||||
let git_remote = repo.find_remote(remote)?;
|
||||
let url = git_remote
|
||||
.pushurl()
|
||||
.or_else(|| git_remote.url())
|
||||
.ok_or_else(|| {
|
||||
crate::error::Error::Generic(format!(
|
||||
"remote '{remote}' has no URL configured"
|
||||
))
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let advertised = advertised_remote_refs(
|
||||
repo_path,
|
||||
Some(remote),
|
||||
&url,
|
||||
basic_credential,
|
||||
)?;
|
||||
let updates = match push {
|
||||
PrePushTarget::Branch { branch, delete } => {
|
||||
let remote_ref =
|
||||
get_remote_ref_for_push(repo_path, branch, *delete)?;
|
||||
vec![pre_push_branch_update(
|
||||
repo_path,
|
||||
branch,
|
||||
&remote_ref,
|
||||
*delete,
|
||||
&advertised,
|
||||
)?]
|
||||
}
|
||||
PrePushTarget::Tags => {
|
||||
pre_push_tag_updates(repo_path, remote, &advertised)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(git2_hooks::hooks_pre_push(
|
||||
&repo,
|
||||
None,
|
||||
Some(remote),
|
||||
&url,
|
||||
&updates,
|
||||
)?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Build a single pre-push update line for a branch.
|
||||
fn pre_push_branch_update(
|
||||
repo_path: &RepoPath,
|
||||
branch_name: &str,
|
||||
remote_ref: &str,
|
||||
delete: bool,
|
||||
advertised: &HashMap<String, Oid>,
|
||||
) -> Result<PrePushRef> {
|
||||
let repo = repo(repo_path)?;
|
||||
let local_ref = format!("refs/heads/{branch_name}");
|
||||
let local_oid = (!delete)
|
||||
.then(|| {
|
||||
repo.find_branch(branch_name, BranchType::Local)
|
||||
.ok()
|
||||
.and_then(|branch| branch.get().peel_to_commit().ok())
|
||||
.map(|commit| commit.id())
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let remote_oid = advertised.get(remote_ref).copied();
|
||||
|
||||
Ok(PrePushRef::new(
|
||||
local_ref, local_oid, remote_ref, remote_oid,
|
||||
))
|
||||
}
|
||||
|
||||
/// Build pre-push updates for tags that are missing on the remote.
|
||||
fn pre_push_tag_updates(
|
||||
repo_path: &RepoPath,
|
||||
remote: &str,
|
||||
advertised: &HashMap<String, Oid>,
|
||||
) -> Result<Vec<PrePushRef>> {
|
||||
let repo = repo(repo_path)?;
|
||||
let tags = tags_missing_remote(repo_path, remote, None)?;
|
||||
let mut updates = Vec::with_capacity(tags.len());
|
||||
|
||||
for tag_ref in tags {
|
||||
if let Ok(reference) = repo.find_reference(&tag_ref) {
|
||||
let tag_oid = reference.target().or_else(|| {
|
||||
reference.peel_to_commit().ok().map(|c| c.id())
|
||||
});
|
||||
let remote_ref = tag_ref.clone();
|
||||
let advertised_oid = advertised.get(&remote_ref).copied();
|
||||
updates.push(PrePushRef::new(
|
||||
tag_ref.clone(),
|
||||
tag_oid,
|
||||
remote_ref,
|
||||
advertised_oid,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
/// What is being pushed.
|
||||
pub enum PrePushTarget<'a> {
|
||||
/// Push a single branch.
|
||||
Branch {
|
||||
/// Local branch name being pushed.
|
||||
branch: &'a str,
|
||||
/// Whether this is a delete push.
|
||||
delete: bool,
|
||||
},
|
||||
/// Push tags.
|
||||
Tags,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::tests::repo_init;
|
||||
use std::fs::{self, File};
|
||||
use std::{ffi::OsString, io::Write as _, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
use git2::Repository;
|
||||
use tempfile::TempDir;
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
|
||||
use super::*;
|
||||
use crate::sync::tests::repo_init_with_prefix;
|
||||
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
fn repo_init() -> Result<(TempDir, Repository)> {
|
||||
let mut os_string: OsString = OsString::new();
|
||||
|
||||
let res = hooks_post_commit(repo_path).unwrap();
|
||||
os_string.push("gitui $# ' ");
|
||||
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
fn create_hook(path: &Path, hook_path: &str, hook_script: &[u8]) {
|
||||
File::create(&path.join(hook_path))
|
||||
.unwrap()
|
||||
.write_all(hook_script)
|
||||
.unwrap();
|
||||
const INVALID_UTF8: &[u8] = b"\xED\xA0\x80";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Command::new("chmod")
|
||||
.args(&["+x", hook_path])
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
os_string.push(std::ffi::OsStr::from_bytes(INVALID_UTF8));
|
||||
|
||||
#[test]
|
||||
fn test_hooks_commit_msg_ok() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
assert!(os_string.to_str().is_none());
|
||||
}
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
exit 0
|
||||
";
|
||||
os_string.push(" ");
|
||||
|
||||
create_hook(root, HOOK_COMMIT_MSG, hook);
|
||||
repo_init_with_prefix(os_string)
|
||||
}
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
|
||||
fn create_hook_in_path(path: &Path, hook_script: &[u8]) {
|
||||
std::fs::File::create(path)
|
||||
.unwrap()
|
||||
.write_all(hook_script)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::process::Command::new("chmod")
|
||||
.arg("+x")
|
||||
.arg(path)
|
||||
// .current_dir(path)
|
||||
.output()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(msg, String::from("test"));
|
||||
}
|
||||
#[test]
|
||||
fn test_post_commit_hook_reject_in_subfolder() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.workdir().unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_pre_commit_sh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
exit 0
|
||||
";
|
||||
git2_hooks::create_hook(
|
||||
&repo,
|
||||
git2_hooks::HOOK_POST_COMMIT,
|
||||
hook,
|
||||
);
|
||||
|
||||
create_hook(root, HOOK_PRE_COMMIT, hook);
|
||||
let res = hooks_pre_commit(repo_path).unwrap();
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
}
|
||||
let subfolder = root.join("foo/");
|
||||
std::fs::create_dir_all(&subfolder).unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_pre_commit_fail_sh() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
let res = hooks_post_commit(&subfolder.into()).unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'rejected'
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
}
|
||||
|
||||
// make sure we run the hooks with the correct pwd.
|
||||
// for non-bare repos this is the dir of the worktree
|
||||
// unfortunately does not work on windows
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_pre_commit_workdir() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.workdir().unwrap();
|
||||
let repo_path: &RepoPath = &root.to_path_buf().into();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo \"$(pwd)\"
|
||||
exit 1
|
||||
";
|
||||
git2_hooks::create_hook(
|
||||
&repo,
|
||||
git2_hooks::HOOK_PRE_COMMIT,
|
||||
hook,
|
||||
);
|
||||
let res = hooks_pre_commit(repo_path).unwrap();
|
||||
if let HookResult::NotOk(res) = res {
|
||||
assert_eq!(
|
||||
res.trim_end().trim_end_matches('/'),
|
||||
// TODO: fix if output isn't utf8.
|
||||
root.to_string_lossy().trim_end_matches('/'),
|
||||
);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_commit_msg_reject_in_subfolder() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.workdir().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'msg' > \"$1\"
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
git2_hooks::create_hook(
|
||||
&repo,
|
||||
git2_hooks::HOOK_COMMIT_MSG,
|
||||
hook,
|
||||
);
|
||||
|
||||
let subfolder = root.join("foo/");
|
||||
std::fs::create_dir_all(&subfolder).unwrap();
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res =
|
||||
hooks_commit_msg(&subfolder.into(), &mut msg).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
|
||||
assert_eq!(msg, String::from("msg\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_commit_msg_reject_in_hooks_folder_githooks_moved_absolute(
|
||||
) {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.workdir().unwrap();
|
||||
let mut config = repo.config().unwrap();
|
||||
|
||||
const HOOKS_DIR: &str = "my_hooks";
|
||||
config.set_str("core.hooksPath", HOOKS_DIR).unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'msg' > \"$1\"
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
let hooks_folder = root.join(HOOKS_DIR);
|
||||
std::fs::create_dir_all(&hooks_folder).unwrap();
|
||||
create_hook_in_path(&hooks_folder.join("commit-msg"), hook);
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res =
|
||||
hooks_commit_msg(&hooks_folder.into(), &mut msg).unwrap();
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
|
||||
assert_eq!(msg, String::from("msg\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_push_hook_rejects_based_on_stdin() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
cat
|
||||
exit 1
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_PRE_COMMIT, hook);
|
||||
let res = hooks_pre_commit(repo_path).unwrap();
|
||||
assert!(res != HookResult::Ok);
|
||||
}
|
||||
git2_hooks::create_hook(
|
||||
&repo,
|
||||
git2_hooks::HOOK_PRE_PUSH,
|
||||
hook,
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_pre_commit_py() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
let commit_id = repo.head().unwrap().target().unwrap();
|
||||
let update = git2_hooks::PrePushRef::new(
|
||||
"refs/heads/master",
|
||||
Some(commit_id),
|
||||
"refs/heads/master",
|
||||
None,
|
||||
);
|
||||
|
||||
// mirror how python pre-commmit sets itself up
|
||||
#[cfg(not(windows))]
|
||||
let hook = b"#!/usr/bin/env python
|
||||
import sys
|
||||
sys.exit(0)
|
||||
";
|
||||
#[cfg(windows)]
|
||||
let hook = b"#!/bin/env python.exe
|
||||
import sys
|
||||
sys.exit(0)
|
||||
";
|
||||
let expected_stdin =
|
||||
git2_hooks::PrePushRef::to_stdin(&[update.clone()]);
|
||||
|
||||
create_hook(root, HOOK_PRE_COMMIT, hook);
|
||||
let res = hooks_pre_commit(repo_path).unwrap();
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
}
|
||||
let res = git2_hooks::hooks_pre_push(
|
||||
&repo,
|
||||
None,
|
||||
Some("origin"),
|
||||
"https://github.com/test/repo.git",
|
||||
&[update],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_pre_commit_fail_py() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
// mirror how python pre-commmit sets itself up
|
||||
#[cfg(not(windows))]
|
||||
let hook = b"#!/usr/bin/env python
|
||||
import sys
|
||||
sys.exit(1)
|
||||
";
|
||||
#[cfg(windows)]
|
||||
let hook = b"#!/bin/env python.exe
|
||||
import sys
|
||||
sys.exit(1)
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_PRE_COMMIT, hook);
|
||||
let res = hooks_pre_commit(repo_path).unwrap();
|
||||
assert!(res != HookResult::Ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_commit_msg_reject() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'msg' > $1
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_COMMIT_MSG, hook);
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
|
||||
assert_eq!(msg, String::from("msg\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hooks_commit_msg_reject_in_subfolder() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
// let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'msg' > $1
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_COMMIT_MSG, hook);
|
||||
|
||||
let subfolder = root.join("foo/");
|
||||
fs::create_dir_all(&subfolder).unwrap();
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res =
|
||||
hooks_commit_msg(subfolder.to_str().unwrap(), &mut msg)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
|
||||
assert_eq!(msg, String::from("msg\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_msg_no_block_but_alter() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'msg' > $1
|
||||
exit 0
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_COMMIT_MSG, hook);
|
||||
|
||||
let mut msg = String::from("test");
|
||||
let res = hooks_commit_msg(repo_path, &mut msg).unwrap();
|
||||
|
||||
assert_eq!(res, HookResult::Ok);
|
||||
assert_eq!(msg, String::from("msg\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_post_commit_hook_reject_in_subfolder() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
|
||||
let hook = b"#!/bin/sh
|
||||
echo 'rejected'
|
||||
exit 1
|
||||
";
|
||||
|
||||
create_hook(root, HOOK_POST_COMMIT, hook);
|
||||
|
||||
let subfolder = root.join("foo/");
|
||||
fs::create_dir_all(&subfolder).unwrap();
|
||||
|
||||
let res =
|
||||
hooks_post_commit(subfolder.to_str().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
HookResult::NotOk(String::from("rejected\n"))
|
||||
);
|
||||
}
|
||||
let git2_hooks::HookResult::Run(response) = res else {
|
||||
panic!("Expected Run result");
|
||||
};
|
||||
assert!(!response.is_successful());
|
||||
assert_eq!(response.stdout, expected_stdin);
|
||||
assert!(expected_stdin.contains("refs/heads/master"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,187 +1,194 @@
|
|||
use super::{
|
||||
diff::{get_diff_raw, HunkHeader},
|
||||
utils::repo,
|
||||
diff::{get_diff_raw, DiffOptions, HunkHeader},
|
||||
RepoPath,
|
||||
};
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
hash,
|
||||
error::{Error, Result},
|
||||
hash,
|
||||
sync::repository::repo,
|
||||
};
|
||||
use git2::{ApplyLocation, ApplyOptions, Diff};
|
||||
use scopetime::scope_time;
|
||||
|
||||
///
|
||||
pub fn stage_hunk(
|
||||
repo_path: &str,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
repo_path: &RepoPath,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
options: Option<DiffOptions>,
|
||||
) -> Result<()> {
|
||||
scope_time!("stage_hunk");
|
||||
scope_time!("stage_hunk");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, file_path, false, false, None)?;
|
||||
let diff = get_diff_raw(&repo, file_path, false, false, options)?;
|
||||
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|hunk| {
|
||||
hunk.map_or(false, |hunk| {
|
||||
let header = HunkHeader::from(hunk);
|
||||
hash(&header) == hunk_hash
|
||||
})
|
||||
});
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|hunk| {
|
||||
hunk.is_some_and(|hunk| {
|
||||
let header = HunkHeader::from(hunk);
|
||||
hash(&header) == hunk_hash
|
||||
})
|
||||
});
|
||||
|
||||
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
|
||||
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// this will fail for an all untracked file
|
||||
pub fn reset_hunk(
|
||||
repo_path: &str,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
repo_path: &RepoPath,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
options: Option<DiffOptions>,
|
||||
) -> Result<()> {
|
||||
scope_time!("reset_hunk");
|
||||
scope_time!("reset_hunk");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, file_path, false, false, None)?;
|
||||
let diff = get_diff_raw(&repo, file_path, false, false, options)?;
|
||||
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
if let Some(hunk_index) = hunk_index {
|
||||
let mut hunk_idx = 0;
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|_hunk| {
|
||||
let res = hunk_idx == hunk_index;
|
||||
hunk_idx += 1;
|
||||
res
|
||||
});
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
if let Some(hunk_index) = hunk_index {
|
||||
let mut hunk_idx = 0;
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|_hunk| {
|
||||
let res = hunk_idx == hunk_index;
|
||||
hunk_idx += 1;
|
||||
res
|
||||
});
|
||||
|
||||
let diff = get_diff_raw(&repo, file_path, false, true, None)?;
|
||||
let diff = get_diff_raw(&repo, file_path, false, true, None)?;
|
||||
|
||||
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
|
||||
repo.apply(&diff, ApplyLocation::WorkDir, Some(&mut opt))?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Generic("hunk not found".to_string()))
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Generic("hunk not found".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_hunk_index(diff: &Diff, hunk_hash: u64) -> Option<usize> {
|
||||
let mut result = None;
|
||||
let mut result = None;
|
||||
|
||||
let mut hunk_count = 0;
|
||||
let mut hunk_count = 0;
|
||||
|
||||
let foreach_result = diff.foreach(
|
||||
&mut |_, _| true,
|
||||
None,
|
||||
Some(&mut |_, hunk| {
|
||||
let header = HunkHeader::from(hunk);
|
||||
if hash(&header) == hunk_hash {
|
||||
result = Some(hunk_count);
|
||||
}
|
||||
hunk_count += 1;
|
||||
true
|
||||
}),
|
||||
None,
|
||||
);
|
||||
let foreach_result = diff.foreach(
|
||||
&mut |_, _| true,
|
||||
None,
|
||||
Some(&mut |_, hunk| {
|
||||
let header = HunkHeader::from(hunk);
|
||||
if hash(&header) == hunk_hash {
|
||||
result = Some(hunk_count);
|
||||
}
|
||||
hunk_count += 1;
|
||||
true
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
if foreach_result.is_ok() {
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if foreach_result.is_ok() {
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn unstage_hunk(
|
||||
repo_path: &str,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
repo_path: &RepoPath,
|
||||
file_path: &str,
|
||||
hunk_hash: u64,
|
||||
options: Option<DiffOptions>,
|
||||
) -> Result<bool> {
|
||||
scope_time!("revert_hunk");
|
||||
scope_time!("revert_hunk");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, file_path, true, false, None)?;
|
||||
let diff_count_positive = diff.deltas().len();
|
||||
let diff = get_diff_raw(&repo, file_path, true, false, options)?;
|
||||
let diff_count_positive = diff.deltas().len();
|
||||
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
let hunk_index = hunk_index.map_or_else(
|
||||
|| Err(Error::Generic("hunk not found".to_string())),
|
||||
Ok,
|
||||
)?;
|
||||
let hunk_index = find_hunk_index(&diff, hunk_hash);
|
||||
let hunk_index = hunk_index.map_or_else(
|
||||
|| Err(Error::Generic("hunk not found".to_string())),
|
||||
Ok,
|
||||
)?;
|
||||
|
||||
let diff = get_diff_raw(&repo, file_path, true, true, None)?;
|
||||
let diff = get_diff_raw(&repo, file_path, true, true, options)?;
|
||||
|
||||
if diff.deltas().len() != diff_count_positive {
|
||||
return Err(Error::Generic(format!(
|
||||
"hunk error: {}!={}",
|
||||
diff.deltas().len(),
|
||||
diff_count_positive
|
||||
)));
|
||||
}
|
||||
if diff.deltas().len() != diff_count_positive {
|
||||
return Err(Error::Generic(format!(
|
||||
"hunk error: {}!={}",
|
||||
diff.deltas().len(),
|
||||
diff_count_positive
|
||||
)));
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
{
|
||||
let mut hunk_idx = 0;
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|_hunk| {
|
||||
let res = if hunk_idx == hunk_index {
|
||||
count += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let mut count = 0;
|
||||
{
|
||||
let mut hunk_idx = 0;
|
||||
let mut opt = ApplyOptions::new();
|
||||
opt.hunk_callback(|_hunk| {
|
||||
let res = if hunk_idx == hunk_index {
|
||||
count += 1;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
hunk_idx += 1;
|
||||
hunk_idx += 1;
|
||||
|
||||
res
|
||||
});
|
||||
res
|
||||
});
|
||||
|
||||
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
|
||||
}
|
||||
repo.apply(&diff, ApplyLocation::Index, Some(&mut opt))?;
|
||||
}
|
||||
|
||||
Ok(count == 1)
|
||||
Ok(count == 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{diff::get_diff, tests::repo_init_empty},
|
||||
};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::Result,
|
||||
sync::{diff::get_diff, tests::repo_init_empty},
|
||||
};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
|
||||
let file_path = Path::new("foo/foo.txt");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn reset_untracked_file_which_will_not_find_hunk() -> Result<()> {
|
||||
let file_path = Path::new("foo/foo.txt");
|
||||
let (_td, repo) = repo_init_empty()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
let sub_path = root.join("foo/");
|
||||
|
||||
let sub_path = root.join("foo/");
|
||||
fs::create_dir_all(&sub_path)?;
|
||||
File::create(root.join(file_path))?.write_all(b"test")?;
|
||||
|
||||
fs::create_dir_all(&sub_path)?;
|
||||
File::create(&root.join(file_path))?.write_all(b"test")?;
|
||||
let sub_path: &RepoPath = &sub_path.to_str().unwrap().into();
|
||||
let diff = get_diff(
|
||||
sub_path,
|
||||
file_path.to_str().unwrap(),
|
||||
false,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let diff = get_diff(
|
||||
sub_path.to_str().unwrap(),
|
||||
file_path.to_str().unwrap(),
|
||||
false,
|
||||
)?;
|
||||
assert!(reset_hunk(
|
||||
repo_path,
|
||||
file_path.to_str().unwrap(),
|
||||
diff.hunks[0].header_hash,
|
||||
None,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
assert!(reset_hunk(
|
||||
repo_path,
|
||||
file_path.to_str().unwrap(),
|
||||
diff.hunks[0].header_hash,
|
||||
)
|
||||
.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,127 +1,159 @@
|
|||
use super::utils::{repo, work_dir};
|
||||
use crate::error::Result;
|
||||
use super::{utils::work_dir, RepoPath};
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::repository::repo,
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
static GITIGNORE: &str = ".gitignore";
|
||||
|
||||
/// add file or path to root ignore file
|
||||
pub fn add_to_ignore(
|
||||
repo_path: &str,
|
||||
path_to_ignore: &str,
|
||||
repo_path: &RepoPath,
|
||||
path_to_ignore: &str,
|
||||
) -> Result<()> {
|
||||
scope_time!("add_to_ignore");
|
||||
scope_time!("add_to_ignore");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let ignore_file = work_dir(&repo)?.join(GITIGNORE);
|
||||
if Path::new(path_to_ignore).file_name()
|
||||
== Path::new(GITIGNORE).file_name()
|
||||
{
|
||||
return Err(Error::Generic(String::from(
|
||||
"cannot ignore gitignore",
|
||||
)));
|
||||
}
|
||||
|
||||
let optional_newline = ignore_file.exists()
|
||||
&& !file_ends_with_newline(&ignore_file)?;
|
||||
let ignore_file = work_dir(&repo)?.join(GITIGNORE);
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(ignore_file)?;
|
||||
let optional_newline = ignore_file.exists()
|
||||
&& !file_ends_with_newline(&ignore_file)?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"{}{}",
|
||||
if optional_newline { "\n" } else { "" },
|
||||
path_to_ignore
|
||||
)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(ignore_file)?;
|
||||
|
||||
Ok(())
|
||||
writeln!(
|
||||
file,
|
||||
"{}{}",
|
||||
if optional_newline { "\n" } else { "" },
|
||||
path_to_ignore
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_ends_with_newline(file: &Path) -> Result<bool> {
|
||||
let mut file = File::open(file)?;
|
||||
let size = file.metadata()?.len();
|
||||
let mut file = File::open(file)?;
|
||||
let size = file.metadata()?.len();
|
||||
|
||||
file.seek(SeekFrom::Start(size.saturating_sub(1)))?;
|
||||
let mut last_char = String::with_capacity(1);
|
||||
file.read_to_string(&mut last_char)?;
|
||||
file.seek(SeekFrom::Start(size.saturating_sub(1)))?;
|
||||
let mut last_char = String::with_capacity(1);
|
||||
file.read_to_string(&mut last_char)?;
|
||||
|
||||
Ok(last_char == "\n")
|
||||
Ok(last_char == "\n")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::tests::repo_init;
|
||||
use io::BufRead;
|
||||
use std::{fs::File, io, path::Path};
|
||||
use super::*;
|
||||
use crate::sync::{tests::repo_init, utils::repo_write_file};
|
||||
use io::BufRead;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{fs::File, io, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_empty() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_empty() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"test")?;
|
||||
File::create(root.join(file_path))?.write_all(b"test")?;
|
||||
|
||||
assert_eq!(root.join(ignore_file_path).exists(), false);
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
assert_eq!(root.join(ignore_file_path).exists(), true);
|
||||
assert_eq!(root.join(ignore_file_path).exists(), false);
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
assert_eq!(root.join(ignore_file_path).exists(), true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_lines<P>(
|
||||
filename: P,
|
||||
) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
fn read_lines<P>(
|
||||
filename: P,
|
||||
) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_append() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"test")?;
|
||||
File::create(&root.join(ignore_file_path))?
|
||||
.write_all(b"foo\n")?;
|
||||
File::create(root.join(file_path))?.write_all(b"test")?;
|
||||
File::create(root.join(ignore_file_path))?
|
||||
.write_all(b"foo\n")?;
|
||||
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
|
||||
let mut lines =
|
||||
read_lines(&root.join(ignore_file_path)).unwrap();
|
||||
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
|
||||
let mut lines =
|
||||
read_lines(root.join(ignore_file_path)).unwrap();
|
||||
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_no_newline_at_end() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_append_no_newline_at_end() -> Result<()> {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let file_path = Path::new("foo.txt");
|
||||
let (_td, repo) = repo_init()?;
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"test")?;
|
||||
File::create(&root.join(ignore_file_path))?
|
||||
.write_all(b"foo")?;
|
||||
File::create(root.join(file_path))?.write_all(b"test")?;
|
||||
File::create(root.join(ignore_file_path))?
|
||||
.write_all(b"foo")?;
|
||||
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
add_to_ignore(repo_path, file_path.to_str().unwrap())?;
|
||||
|
||||
let mut lines =
|
||||
read_lines(&root.join(ignore_file_path)).unwrap();
|
||||
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
|
||||
let mut lines =
|
||||
read_lines(root.join(ignore_file_path)).unwrap();
|
||||
assert_eq!(&lines.nth(1).unwrap().unwrap(), "foo.txt");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_ignore() {
|
||||
let ignore_file_path = Path::new(".gitignore");
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
repo_write_file(&repo, ".gitignore", "#foo").unwrap();
|
||||
|
||||
let res = add_to_ignore(repo_path, ".gitignore");
|
||||
assert!(res.is_err());
|
||||
|
||||
let lines = read_lines(root.join(ignore_file_path)).unwrap();
|
||||
assert_eq!(lines.count(), 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,248 +1,386 @@
|
|||
use super::CommitId;
|
||||
use super::{CommitId, SharedCommitFilterFn};
|
||||
use crate::error::Result;
|
||||
use git2::{Commit, Oid, Repository};
|
||||
use gix::revision::Walk;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BinaryHeap, HashSet},
|
||||
sync::Arc,
|
||||
cmp::Ordering,
|
||||
collections::{BinaryHeap, HashSet},
|
||||
};
|
||||
|
||||
struct TimeOrderedCommit<'a>(Commit<'a>);
|
||||
|
||||
impl<'a> Eq for TimeOrderedCommit<'a> {}
|
||||
impl Eq for TimeOrderedCommit<'_> {}
|
||||
|
||||
impl<'a> PartialEq for TimeOrderedCommit<'a> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.time().eq(&other.0.time())
|
||||
}
|
||||
impl PartialEq for TimeOrderedCommit<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.time().eq(&other.0.time())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialOrd for TimeOrderedCommit<'a> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.time().partial_cmp(&other.0.time())
|
||||
}
|
||||
impl PartialOrd for TimeOrderedCommit<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ord for TimeOrderedCommit<'a> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.time().cmp(&other.0.time())
|
||||
}
|
||||
impl Ord for TimeOrderedCommit<'_> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.time().cmp(&other.0.time())
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub type LogWalkerFilter = Arc<
|
||||
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
|
||||
>;
|
||||
|
||||
///
|
||||
pub struct LogWalker<'a> {
|
||||
commits: BinaryHeap<TimeOrderedCommit<'a>>,
|
||||
visited: HashSet<Oid>,
|
||||
limit: usize,
|
||||
repo: &'a Repository,
|
||||
filter: Option<LogWalkerFilter>,
|
||||
commits: BinaryHeap<TimeOrderedCommit<'a>>,
|
||||
visited: HashSet<Oid>,
|
||||
limit: usize,
|
||||
repo: &'a Repository,
|
||||
filter: Option<SharedCommitFilterFn>,
|
||||
}
|
||||
|
||||
impl<'a> LogWalker<'a> {
|
||||
///
|
||||
pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {
|
||||
let c = repo.head()?.peel_to_commit()?;
|
||||
///
|
||||
pub fn new(repo: &'a Repository, limit: usize) -> Result<Self> {
|
||||
let c = repo.head()?.peel_to_commit()?;
|
||||
|
||||
let mut commits = BinaryHeap::with_capacity(10);
|
||||
commits.push(TimeOrderedCommit(c));
|
||||
let mut commits = BinaryHeap::with_capacity(10);
|
||||
commits.push(TimeOrderedCommit(c));
|
||||
|
||||
Ok(Self {
|
||||
commits,
|
||||
limit,
|
||||
visited: HashSet::with_capacity(1000),
|
||||
repo,
|
||||
filter: None,
|
||||
})
|
||||
}
|
||||
Ok(Self {
|
||||
commits,
|
||||
limit,
|
||||
visited: HashSet::with_capacity(1000),
|
||||
repo,
|
||||
filter: None,
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub fn filter(self, filter: Option<LogWalkerFilter>) -> Self {
|
||||
Self { filter, ..self }
|
||||
}
|
||||
///
|
||||
pub fn visited(&self) -> usize {
|
||||
self.visited.len()
|
||||
}
|
||||
|
||||
///
|
||||
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
|
||||
let mut count = 0_usize;
|
||||
///
|
||||
#[must_use]
|
||||
pub fn filter(
|
||||
self,
|
||||
filter: Option<SharedCommitFilterFn>,
|
||||
) -> Self {
|
||||
Self { filter, ..self }
|
||||
}
|
||||
|
||||
while let Some(c) = self.commits.pop() {
|
||||
for p in c.0.parents() {
|
||||
self.visit(p);
|
||||
}
|
||||
///
|
||||
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
|
||||
let mut count = 0_usize;
|
||||
|
||||
let id: CommitId = c.0.id().into();
|
||||
let commit_should_be_included =
|
||||
if let Some(ref filter) = self.filter {
|
||||
filter(self.repo, &id)?
|
||||
} else {
|
||||
true
|
||||
};
|
||||
while let Some(c) = self.commits.pop() {
|
||||
for p in c.0.parents() {
|
||||
self.visit(p);
|
||||
}
|
||||
|
||||
if commit_should_be_included {
|
||||
out.push(id);
|
||||
}
|
||||
let id: CommitId = c.0.id().into();
|
||||
let commit_should_be_included =
|
||||
if let Some(ref filter) = self.filter {
|
||||
filter(self.repo, &id)?
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
count += 1;
|
||||
if count == self.limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if commit_should_be_included {
|
||||
out.push(id);
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
count += 1;
|
||||
if count == self.limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
fn visit(&mut self, c: Commit<'a>) {
|
||||
if !self.visited.contains(&c.id()) {
|
||||
self.visited.insert(c.id());
|
||||
self.commits.push(TimeOrderedCommit(c));
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
//
|
||||
fn visit(&mut self, c: Commit<'a>) {
|
||||
if self.visited.insert(c.id()) {
|
||||
self.commits.push(TimeOrderedCommit(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is separate from `LogWalker` because filtering currently (June 2024) works through
|
||||
/// `SharedCommitFilterFn`.
|
||||
///
|
||||
/// `SharedCommitFilterFn` requires access to a `git2::repo::Repository` because, under the hood,
|
||||
/// it calls into functions that work with a `git2::repo::Repository`. It seems unwise to open a
|
||||
/// repo both through `gix::discover` and `Repository::open_ext` at the same time, so there is a
|
||||
/// separate struct that works with `gix::Repository` only.
|
||||
///
|
||||
/// A more long-term option is to refactor filtering to work with a `gix::Repository` and to remove
|
||||
/// `LogWalker` once this is done, but this is a larger effort.
|
||||
pub struct LogWalkerWithoutFilter<'a> {
|
||||
walk: Walk<'a>,
|
||||
limit: usize,
|
||||
visited: usize,
|
||||
}
|
||||
|
||||
impl<'a> LogWalkerWithoutFilter<'a> {
|
||||
///
|
||||
pub fn new(
|
||||
repo: &'a mut gix::Repository,
|
||||
limit: usize,
|
||||
) -> Result<Self> {
|
||||
// This seems to be an object cache size that yields optimal performance. There’s no specific
|
||||
// reason this is 2^14, so benchmarking might reveal that there’s better values.
|
||||
repo.object_cache_size_if_unset(2_usize.pow(14));
|
||||
|
||||
let commit = repo.head()?.peel_to_commit()?;
|
||||
|
||||
let tips = [commit.id];
|
||||
|
||||
let platform = repo
|
||||
.rev_walk(tips)
|
||||
.sorting(gix::revision::walk::Sorting::ByCommitTime(gix::traverse::commit::simple::CommitTimeOrder::NewestFirst))
|
||||
.use_commit_graph(false);
|
||||
|
||||
let walk = platform.all()?;
|
||||
|
||||
Ok(Self {
|
||||
walk,
|
||||
limit,
|
||||
visited: 0,
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
pub const fn visited(&self) -> usize {
|
||||
self.visited
|
||||
}
|
||||
|
||||
///
|
||||
pub fn read(&mut self, out: &mut Vec<CommitId>) -> Result<usize> {
|
||||
let mut count = 0_usize;
|
||||
|
||||
while let Some(Ok(info)) = self.walk.next() {
|
||||
out.push(info.id.into());
|
||||
|
||||
count += 1;
|
||||
|
||||
if count == self.limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.visited += count;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Result;
|
||||
use crate::sync::{
|
||||
commit, commit_files::get_commit_diff, get_commits_info,
|
||||
stage_add_file, tests::repo_init_empty,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use super::*;
|
||||
use crate::error::Result;
|
||||
use crate::sync::commit_filter::{SearchFields, SearchOptions};
|
||||
use crate::sync::repository::gix_repo;
|
||||
use crate::sync::tests::write_commit_file;
|
||||
use crate::sync::{
|
||||
commit, get_commits_info, stage_add_file,
|
||||
tests::repo_init_empty,
|
||||
};
|
||||
use crate::sync::{
|
||||
diff_contains_file, filter_commit_by_search, LogFilterSearch,
|
||||
LogFilterSearchOptions, RepoPath,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
#[test]
|
||||
fn test_limit() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_limit() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit1").unwrap();
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let oid2 = commit(repo_path, "commit2").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit1").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let oid2 = commit(repo_path, "commit2").unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo, 1)?;
|
||||
walk.read(&mut items).unwrap();
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo, 1)?;
|
||||
walk.read(&mut items).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], oid2.into());
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], oid2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logwalker() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_logwalker() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit1").unwrap();
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let oid2 = commit(repo_path, "commit2").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit1").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let oid2 = commit(repo_path, "commit2").unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo, 100)?;
|
||||
walk.read(&mut items).unwrap();
|
||||
let mut items = Vec::new();
|
||||
let mut walk = LogWalker::new(&repo, 100)?;
|
||||
walk.read(&mut items).unwrap();
|
||||
|
||||
let info = get_commits_info(repo_path, &items, 50).unwrap();
|
||||
dbg!(&info);
|
||||
let info = get_commits_info(repo_path, &items, 50).unwrap();
|
||||
dbg!(&info);
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0], oid2.into());
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0], oid2);
|
||||
|
||||
let mut items = Vec::new();
|
||||
walk.read(&mut items).unwrap();
|
||||
let mut items = Vec::new();
|
||||
walk.read(&mut items).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 0);
|
||||
assert_eq!(items.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logwalker_with_filter() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let second_file_path = Path::new("baz");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_logwalker_without_filter() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
commit(repo_path, "commit1").unwrap();
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let oid2 = commit(repo_path, "commit2").unwrap();
|
||||
|
||||
let _first_commit_id = commit(repo_path, "commit1").unwrap();
|
||||
let mut repo: gix::Repository = gix_repo(repo_path)?;
|
||||
let mut walk = LogWalkerWithoutFilter::new(&mut repo, 100)?;
|
||||
let mut items = Vec::new();
|
||||
assert!(matches!(walk.read(&mut items), Ok(2)));
|
||||
|
||||
File::create(&root.join(second_file_path))?
|
||||
.write_all(b"a")?;
|
||||
stage_add_file(repo_path, second_file_path).unwrap();
|
||||
let info = get_commits_info(repo_path, &items, 50).unwrap();
|
||||
dbg!(&info);
|
||||
|
||||
let second_commit_id = commit(repo_path, "commit2").unwrap();
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0], oid2);
|
||||
|
||||
File::create(&root.join(file_path))?.write_all(b"b")?;
|
||||
stage_add_file(repo_path, file_path).unwrap();
|
||||
let mut items = Vec::new();
|
||||
assert!(matches!(walk.read(&mut items), Ok(0)));
|
||||
|
||||
let _third_commit_id = commit(repo_path, "commit3").unwrap();
|
||||
assert_eq!(items.len(), 0);
|
||||
|
||||
let diff_contains_baz = |repo: &Repository,
|
||||
commit_id: &CommitId|
|
||||
-> Result<bool> {
|
||||
let diff = get_commit_diff(
|
||||
&repo,
|
||||
*commit_id,
|
||||
Some("baz".into()),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let contains_file = diff.deltas().len() > 0;
|
||||
#[test]
|
||||
fn test_logwalker_with_filter() -> Result<()> {
|
||||
let file_path = Path::new("foo");
|
||||
let second_file_path = Path::new("baz");
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: RepoPath =
|
||||
root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
Ok(contains_file)
|
||||
};
|
||||
File::create(root.join(file_path))?.write_all(b"a")?;
|
||||
stage_add_file(&repo_path, file_path).unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)?
|
||||
.filter(Some(Arc::new(Box::new(diff_contains_baz))));
|
||||
walker.read(&mut items).unwrap();
|
||||
let _first_commit_id = commit(&repo_path, "commit1").unwrap();
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], second_commit_id.into());
|
||||
File::create(root.join(second_file_path))?.write_all(b"a")?;
|
||||
stage_add_file(&repo_path, second_file_path).unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
walker.read(&mut items).unwrap();
|
||||
let second_commit_id = commit(&repo_path, "commit2").unwrap();
|
||||
|
||||
assert_eq!(items.len(), 0);
|
||||
File::create(root.join(file_path))?.write_all(b"b")?;
|
||||
stage_add_file(&repo_path, file_path).unwrap();
|
||||
|
||||
let diff_contains_bar = |repo: &Repository,
|
||||
commit_id: &CommitId|
|
||||
-> Result<bool> {
|
||||
let diff = get_commit_diff(
|
||||
&repo,
|
||||
*commit_id,
|
||||
Some("bar".into()),
|
||||
)?;
|
||||
let _third_commit_id = commit(&repo_path, "commit3").unwrap();
|
||||
|
||||
let contains_file = diff.deltas().len() > 0;
|
||||
let diff_contains_baz = diff_contains_file("baz".into());
|
||||
|
||||
Ok(contains_file)
|
||||
};
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)?
|
||||
.filter(Some(diff_contains_baz));
|
||||
walker.read(&mut items).unwrap();
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)?
|
||||
.filter(Some(Arc::new(Box::new(diff_contains_bar))));
|
||||
walker.read(&mut items).unwrap();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], second_commit_id);
|
||||
|
||||
assert_eq!(items.len(), 0);
|
||||
let mut items = Vec::new();
|
||||
walker.read(&mut items).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
assert_eq!(items.len(), 0);
|
||||
|
||||
let diff_contains_bar = diff_contains_file("bar".into());
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)?
|
||||
.filter(Some(diff_contains_bar));
|
||||
walker.read(&mut items).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_logwalker_with_filter_search() {
|
||||
let (_td, repo) = repo_init_empty().unwrap();
|
||||
|
||||
write_commit_file(&repo, "foo", "a", "commit1");
|
||||
let second_commit_id = write_commit_file(
|
||||
&repo,
|
||||
"baz",
|
||||
"a",
|
||||
"my commit msg (#2)",
|
||||
);
|
||||
write_commit_file(&repo, "foo", "b", "commit3");
|
||||
|
||||
let log_filter = filter_commit_by_search(
|
||||
LogFilterSearch::new(LogFilterSearchOptions {
|
||||
fields: SearchFields::MESSAGE_SUMMARY,
|
||||
options: SearchOptions::FUZZY_SEARCH,
|
||||
search_pattern: String::from("my msg"),
|
||||
}),
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)
|
||||
.unwrap()
|
||||
.filter(Some(log_filter));
|
||||
walker.read(&mut items).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0], second_commit_id);
|
||||
|
||||
let log_filter = filter_commit_by_search(
|
||||
LogFilterSearch::new(LogFilterSearchOptions {
|
||||
fields: SearchFields::FILENAMES,
|
||||
options: SearchOptions::FUZZY_SEARCH,
|
||||
search_pattern: String::from("fo"),
|
||||
}),
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut walker = LogWalker::new(&repo, 100)
|
||||
.unwrap()
|
||||
.filter(Some(log_filter));
|
||||
walker.read(&mut items).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,141 +1,188 @@
|
|||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
branch::merge_commit::commit_merge_with_head, reset_stage,
|
||||
reset_workdir, utils, CommitId,
|
||||
},
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
branch::merge_commit::commit_merge_with_head,
|
||||
rebase::{
|
||||
abort_rebase, continue_rebase, get_rebase_progress,
|
||||
},
|
||||
repository::repo,
|
||||
reset_stage, reset_workdir, CommitId,
|
||||
},
|
||||
};
|
||||
use git2::{BranchType, Commit, MergeOptions, Repository};
|
||||
use scopetime::scope_time;
|
||||
|
||||
use super::{
|
||||
rebase::{RebaseProgress, RebaseState},
|
||||
RepoPath,
|
||||
};
|
||||
|
||||
///
|
||||
pub fn mergehead_ids(repo_path: &str) -> Result<Vec<CommitId>> {
|
||||
scope_time!("mergehead_ids");
|
||||
pub fn mergehead_ids(repo_path: &RepoPath) -> Result<Vec<CommitId>> {
|
||||
scope_time!("mergehead_ids");
|
||||
|
||||
let mut repo = utils::repo(repo_path)?;
|
||||
let mut repo = repo(repo_path)?;
|
||||
|
||||
let mut ids: Vec<CommitId> = Vec::new();
|
||||
repo.mergehead_foreach(|id| {
|
||||
ids.push(CommitId::from(*id));
|
||||
true
|
||||
})?;
|
||||
let mut ids: Vec<CommitId> = Vec::new();
|
||||
repo.mergehead_foreach(|id| {
|
||||
ids.push(CommitId::from(*id));
|
||||
true
|
||||
})?;
|
||||
|
||||
Ok(ids)
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// does these steps:
|
||||
/// * reset all staged changes,
|
||||
/// * revert all changes in workdir
|
||||
/// * cleanup repo merge state
|
||||
pub fn abort_merge(repo_path: &str) -> Result<()> {
|
||||
scope_time!("cleanup_state");
|
||||
pub fn abort_pending_state(repo_path: &RepoPath) -> Result<()> {
|
||||
scope_time!("abort_pending_state");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
reset_stage(repo_path, "*")?;
|
||||
reset_workdir(repo_path, "*")?;
|
||||
reset_stage(repo_path, "*")?;
|
||||
reset_workdir(repo_path, "*")?;
|
||||
|
||||
repo.cleanup_state()?;
|
||||
repo.cleanup_state()?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn merge_branch(repo_path: &str, branch: &str) -> Result<()> {
|
||||
scope_time!("merge_branch");
|
||||
pub fn merge_branch(
|
||||
repo_path: &RepoPath,
|
||||
branch: &str,
|
||||
branch_type: BranchType,
|
||||
) -> Result<()> {
|
||||
scope_time!("merge_branch");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
merge_branch_repo(&repo, branch)?;
|
||||
merge_branch_repo(&repo, branch, branch_type)?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn rebase_progress(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<RebaseProgress> {
|
||||
scope_time!("rebase_progress");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
get_rebase_progress(&repo)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn continue_pending_rebase(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<RebaseState> {
|
||||
scope_time!("continue_pending_rebase");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
continue_rebase(&repo)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn abort_pending_rebase(repo_path: &RepoPath) -> Result<()> {
|
||||
scope_time!("abort_pending_rebase");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
abort_rebase(&repo)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn merge_branch_repo(
|
||||
repo: &Repository,
|
||||
branch: &str,
|
||||
repo: &Repository,
|
||||
branch: &str,
|
||||
branch_type: BranchType,
|
||||
) -> Result<()> {
|
||||
let branch = repo.find_branch(branch, BranchType::Local)?;
|
||||
let branch = repo.find_branch(branch, branch_type)?;
|
||||
|
||||
let annotated =
|
||||
repo.reference_to_annotated_commit(&branch.into_reference())?;
|
||||
let annotated =
|
||||
repo.reference_to_annotated_commit(&branch.into_reference())?;
|
||||
|
||||
let (analysis, _) = repo.merge_analysis(&[&annotated])?;
|
||||
let (analysis, _) = repo.merge_analysis(&[&annotated])?;
|
||||
|
||||
//TODO: support merge on unborn
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
//TODO: support merge on unborn
|
||||
if analysis.is_unborn() {
|
||||
return Err(Error::Generic("head is unborn".into()));
|
||||
}
|
||||
|
||||
let mut opt = MergeOptions::default();
|
||||
let mut opt = MergeOptions::default();
|
||||
|
||||
repo.merge(&[&annotated], Some(&mut opt), None)?;
|
||||
repo.merge(&[&annotated], Some(&mut opt), None)?;
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn merge_msg(repo_path: &str) -> Result<String> {
|
||||
scope_time!("merge_msg");
|
||||
pub fn merge_msg(repo_path: &RepoPath) -> Result<String> {
|
||||
scope_time!("merge_msg");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let content = repo.message()?;
|
||||
let repo = repo(repo_path)?;
|
||||
let content = repo.message()?;
|
||||
|
||||
Ok(content)
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn merge_commit(
|
||||
repo_path: &str,
|
||||
msg: &str,
|
||||
ids: &[CommitId],
|
||||
repo_path: &RepoPath,
|
||||
msg: &str,
|
||||
ids: &[CommitId],
|
||||
) -> Result<CommitId> {
|
||||
scope_time!("merge_commit");
|
||||
scope_time!("merge_commit");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let mut commits: Vec<Commit> = Vec::new();
|
||||
let mut commits: Vec<Commit> = Vec::new();
|
||||
|
||||
for id in ids {
|
||||
commits.push(repo.find_commit((*id).into())?);
|
||||
}
|
||||
for id in ids {
|
||||
commits.push(repo.find_commit((*id).into())?);
|
||||
}
|
||||
|
||||
let id = commit_merge_with_head(&repo, &commits, msg)?;
|
||||
let id = commit_merge_with_head(&repo, &commits, msg)?;
|
||||
|
||||
Ok(id)
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
create_branch,
|
||||
tests::{repo_init, write_commit_file},
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use super::*;
|
||||
use crate::sync::{
|
||||
create_branch,
|
||||
tests::{repo_init, write_commit_file},
|
||||
RepoPath,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path = root.as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let c1 =
|
||||
write_commit_file(&repo, "test.txt", "test", "commit1");
|
||||
let c1 =
|
||||
write_commit_file(&repo, "test.txt", "test", "commit1");
|
||||
|
||||
create_branch(repo_path, "foo").unwrap();
|
||||
create_branch(repo_path, "foo").unwrap();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test2", "commit2");
|
||||
write_commit_file(&repo, "test.txt", "test2", "commit2");
|
||||
|
||||
merge_branch(repo_path, "master").unwrap();
|
||||
merge_branch(repo_path, "master", BranchType::Local).unwrap();
|
||||
|
||||
let msg = merge_msg(repo_path).unwrap();
|
||||
let msg = merge_msg(repo_path).unwrap();
|
||||
|
||||
assert_eq!(&msg[0..12], "Merge branch");
|
||||
assert_eq!(&msg[0..12], "Merge branch");
|
||||
|
||||
let mergeheads = mergehead_ids(repo_path).unwrap();
|
||||
let mergeheads = mergehead_ids(repo_path).unwrap();
|
||||
|
||||
assert_eq!(mergeheads[0], c1);
|
||||
}
|
||||
assert_eq!(mergeheads[0], c1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
|
||||
pub mod blame;
|
||||
pub mod branch;
|
||||
mod commit;
|
||||
pub mod commit;
|
||||
mod commit_details;
|
||||
mod commit_files;
|
||||
pub mod commit_files;
|
||||
mod commit_filter;
|
||||
mod commit_revert;
|
||||
mod commits_info;
|
||||
mod config;
|
||||
pub mod cred;
|
||||
|
|
@ -18,300 +20,361 @@ mod ignore;
|
|||
mod logwalker;
|
||||
mod merge;
|
||||
mod patches;
|
||||
mod rebase;
|
||||
pub mod remotes;
|
||||
mod repository;
|
||||
mod reset;
|
||||
mod reword;
|
||||
pub mod sign;
|
||||
mod staging;
|
||||
mod stash;
|
||||
mod state;
|
||||
pub mod status;
|
||||
mod submodules;
|
||||
mod tags;
|
||||
mod tree;
|
||||
pub mod utils;
|
||||
|
||||
pub use blame::{blame_file, BlameHunk, FileBlame};
|
||||
pub use branch::{
|
||||
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
|
||||
create_branch, delete_branch, get_branch_remote,
|
||||
get_branches_info, merge_commit::merge_upstream_commit,
|
||||
merge_ff::branch_merge_upstream_fastforward,
|
||||
merge_rebase::merge_upstream_rebase, rename::rename_branch,
|
||||
BranchCompare, BranchInfo,
|
||||
branch_compare_upstream, checkout_branch, checkout_commit,
|
||||
config_is_pull_rebase, create_branch, delete_branch,
|
||||
get_branch_remote, get_branch_upstream_merge, get_branches_info,
|
||||
merge_commit::merge_upstream_commit,
|
||||
merge_ff::branch_merge_upstream_fastforward,
|
||||
merge_rebase::merge_upstream_rebase, rename::rename_branch,
|
||||
validate_branch_name, BranchCompare, BranchDetails, BranchInfo,
|
||||
};
|
||||
pub use commit::{amend, commit, tag};
|
||||
pub use commit::{amend, commit, tag_commit};
|
||||
pub use commit_details::{
|
||||
get_commit_details, CommitDetails, CommitMessage, CommitSignature,
|
||||
get_commit_details, CommitDetails, CommitMessage, CommitSignature,
|
||||
};
|
||||
pub use commit_files::get_commit_files;
|
||||
pub use commit_filter::{
|
||||
diff_contains_file, filter_commit_by_search, LogFilterSearch,
|
||||
LogFilterSearchOptions, SearchFields, SearchOptions,
|
||||
SharedCommitFilterFn,
|
||||
};
|
||||
pub use commit_revert::{commit_revert, revert_commit, revert_head};
|
||||
pub use commits_info::{
|
||||
get_commit_info, get_commits_info, CommitId, CommitInfo,
|
||||
get_commit_info, get_commits_info, CommitId, CommitInfo,
|
||||
};
|
||||
pub use config::{
|
||||
get_config_string, untracked_files_config,
|
||||
ShowUntrackedFilesConfig,
|
||||
get_config_string, untracked_files_config,
|
||||
ShowUntrackedFilesConfig,
|
||||
};
|
||||
pub use diff::get_diff_commit;
|
||||
pub use git2::BranchType;
|
||||
pub use hooks::{
|
||||
hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,
|
||||
hooks_commit_msg, hooks_post_commit, hooks_pre_commit,
|
||||
hooks_pre_push, hooks_prepare_commit_msg, HookResult,
|
||||
PrePushTarget, PrepareCommitMsgSource,
|
||||
};
|
||||
pub use hunks::{reset_hunk, stage_hunk, unstage_hunk};
|
||||
pub use ignore::add_to_ignore;
|
||||
pub use logwalker::{LogWalker, LogWalkerFilter};
|
||||
pub use logwalker::{LogWalker, LogWalkerWithoutFilter};
|
||||
pub use merge::{
|
||||
abort_merge, merge_branch, merge_commit, merge_msg, mergehead_ids,
|
||||
abort_pending_rebase, abort_pending_state,
|
||||
continue_pending_rebase, merge_branch, merge_commit, merge_msg,
|
||||
mergehead_ids, rebase_progress,
|
||||
};
|
||||
pub use rebase::rebase_branch;
|
||||
pub use remotes::{
|
||||
get_default_remote, get_remotes, push::AsyncProgress,
|
||||
tags::PushTagsProgress,
|
||||
add_remote, delete_remote, get_default_remote,
|
||||
get_default_remote_for_fetch, get_default_remote_for_push,
|
||||
get_remote_url, get_remotes, push::AsyncProgress, rename_remote,
|
||||
tags::PushTagsProgress, update_remote_url, validate_remote_name,
|
||||
};
|
||||
pub use reset::{reset_stage, reset_workdir};
|
||||
pub(crate) use repository::{gix_repo, repo};
|
||||
pub use repository::{RepoPath, RepoPathRef};
|
||||
pub use reset::{reset_repo, reset_stage, reset_workdir};
|
||||
pub use reword::reword;
|
||||
pub use staging::{discard_lines, stage_lines};
|
||||
pub use stash::{
|
||||
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
|
||||
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
|
||||
};
|
||||
pub use state::{repo_state, RepoState};
|
||||
pub use status::is_workdir_clean;
|
||||
pub use submodules::{
|
||||
get_submodules, submodule_parent_info, update_submodule,
|
||||
SubmoduleInfo, SubmoduleParentInfo, SubmoduleStatus,
|
||||
};
|
||||
pub use tags::{
|
||||
delete_tag, get_tags, get_tags_with_metadata, CommitTags,
|
||||
TagWithMetadata, Tags,
|
||||
delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag,
|
||||
TagWithMetadata, Tags,
|
||||
};
|
||||
pub use tree::{tree_file_content, tree_files, TreeFile};
|
||||
pub use utils::{
|
||||
get_head, get_head_tuple, is_bare_repo, is_repo, repo_dir,
|
||||
stage_add_all, stage_add_file, stage_addremoved, Head,
|
||||
get_head, get_head_tuple, repo_dir, repo_open_error,
|
||||
stage_add_all, stage_add_file, stage_addremoved, Head,
|
||||
};
|
||||
|
||||
pub use git2::ResetType;
|
||||
|
||||
/// test utils
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
commit, stage_add_file,
|
||||
status::{get_status, StatusType},
|
||||
utils::{get_head_repo, repo, repo_write_file},
|
||||
CommitId, LogWalker,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use git2::Repository;
|
||||
use std::{path::Path, process::Command};
|
||||
use tempfile::TempDir;
|
||||
pub mod tests {
|
||||
use super::{
|
||||
commit,
|
||||
repository::repo,
|
||||
stage_add_file,
|
||||
status::{get_status, StatusType},
|
||||
utils::{get_head_repo, repo_write_file},
|
||||
CommitId, LogWalker, RepoPath,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use git2::Repository;
|
||||
use std::{ffi::OsStr, path::Path, process::Command};
|
||||
use tempfile::TempDir;
|
||||
|
||||
/// Calling `set_search_path` with an empty directory makes sure that there
|
||||
/// is no git config interfering with our tests (for example user-local
|
||||
/// `.gitconfig`).
|
||||
#[allow(unsafe_code)]
|
||||
fn sandbox_config_files() {
|
||||
use git2::{opts::set_search_path, ConfigLevel};
|
||||
use std::sync::Once;
|
||||
///
|
||||
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
sandbox_config_files();
|
||||
|
||||
// Adapted from https://github.com/rust-lang/cargo/pull/9035
|
||||
INIT.call_once(|| unsafe {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let path = temp_dir.path();
|
||||
let td = TempDir::new()?;
|
||||
let repo = Repository::init(td.path())?;
|
||||
{
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
}
|
||||
Ok((td, repo))
|
||||
}
|
||||
|
||||
set_search_path(ConfigLevel::System, &path).unwrap();
|
||||
set_search_path(ConfigLevel::Global, &path).unwrap();
|
||||
set_search_path(ConfigLevel::XDG, &path).unwrap();
|
||||
set_search_path(ConfigLevel::ProgramData, &path).unwrap();
|
||||
});
|
||||
}
|
||||
///
|
||||
pub fn repo_init() -> Result<(TempDir, Repository)> {
|
||||
repo_init_with_prefix("gitui")
|
||||
}
|
||||
|
||||
/// write, stage and commit a file
|
||||
pub fn write_commit_file(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
) -> CommitId {
|
||||
repo_write_file(repo, file, content).unwrap();
|
||||
///
|
||||
#[inline]
|
||||
pub fn repo_init_with_prefix(
|
||||
prefix: impl AsRef<OsStr>,
|
||||
) -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
stage_add_file(
|
||||
repo.workdir().unwrap().to_str().unwrap(),
|
||||
Path::new(file),
|
||||
)
|
||||
.unwrap();
|
||||
sandbox_config_files();
|
||||
|
||||
commit(repo.workdir().unwrap().to_str().unwrap(), commit_name)
|
||||
.unwrap()
|
||||
}
|
||||
let td = TempDir::with_prefix(prefix)?;
|
||||
let repo = Repository::init(td.path())?;
|
||||
{
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
|
||||
/// write, stage and commit a file giving the commit a specific timestamp
|
||||
pub fn write_commit_file_at(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
time: git2::Time,
|
||||
) -> CommitId {
|
||||
repo_write_file(repo, file, content).unwrap();
|
||||
let mut index = repo.index()?;
|
||||
let id = index.write_tree()?;
|
||||
|
||||
let path = repo.workdir().unwrap().to_str().unwrap();
|
||||
let tree = repo.find_tree(id)?;
|
||||
let sig = repo.signature()?;
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
"initial",
|
||||
&tree,
|
||||
&[],
|
||||
)?;
|
||||
}
|
||||
Ok((td, repo))
|
||||
}
|
||||
|
||||
stage_add_file(path, Path::new(file)).unwrap();
|
||||
///
|
||||
pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
|
||||
sandbox_config_files();
|
||||
|
||||
commit_at(path, commit_name, time)
|
||||
}
|
||||
let td = TempDir::new()?;
|
||||
|
||||
fn commit_at(
|
||||
repo_path: &str,
|
||||
msg: &str,
|
||||
time: git2::Time,
|
||||
) -> CommitId {
|
||||
let repo = repo(repo_path).unwrap();
|
||||
let td_path = td.path().as_os_str().to_str().unwrap();
|
||||
|
||||
let signature =
|
||||
git2::Signature::new("name", "email", &time).unwrap();
|
||||
let mut index = repo.index().unwrap();
|
||||
let tree_id = index.write_tree().unwrap();
|
||||
let tree = repo.find_tree(tree_id).unwrap();
|
||||
let repo = Repository::clone(p, td_path).unwrap();
|
||||
|
||||
let parents = if let Ok(id) = get_head_repo(&repo) {
|
||||
vec![repo.find_commit(id.into()).unwrap()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
|
||||
let parents = parents.iter().collect::<Vec<_>>();
|
||||
Ok((td, repo))
|
||||
}
|
||||
|
||||
let commit = repo
|
||||
.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)
|
||||
.unwrap()
|
||||
.into();
|
||||
/// write, stage and commit a file
|
||||
pub fn write_commit_file(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
) -> CommitId {
|
||||
repo_write_file(repo, file, content).unwrap();
|
||||
|
||||
commit
|
||||
}
|
||||
stage_add_file(
|
||||
&repo.workdir().unwrap().to_str().unwrap().into(),
|
||||
Path::new(file),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
///
|
||||
pub fn repo_init_empty() -> Result<(TempDir, Repository)> {
|
||||
sandbox_config_files();
|
||||
commit(
|
||||
&repo.workdir().unwrap().to_str().unwrap().into(),
|
||||
commit_name,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
let td = TempDir::new()?;
|
||||
let repo = Repository::init(td.path())?;
|
||||
{
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
}
|
||||
Ok((td, repo))
|
||||
}
|
||||
/// write, stage and commit a file giving the commit a specific timestamp
|
||||
pub fn write_commit_file_at(
|
||||
repo: &Repository,
|
||||
file: &str,
|
||||
content: &str,
|
||||
commit_name: &str,
|
||||
time: git2::Time,
|
||||
) -> CommitId {
|
||||
repo_write_file(repo, file, content).unwrap();
|
||||
|
||||
///
|
||||
pub fn repo_init() -> Result<(TempDir, Repository)> {
|
||||
sandbox_config_files();
|
||||
let path: &RepoPath =
|
||||
&repo.workdir().unwrap().to_str().unwrap().into();
|
||||
|
||||
let td = TempDir::new()?;
|
||||
let repo = Repository::init(td.path())?;
|
||||
{
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
stage_add_file(path, Path::new(file)).unwrap();
|
||||
|
||||
let mut index = repo.index()?;
|
||||
let id = index.write_tree()?;
|
||||
commit_at(path, commit_name, time)
|
||||
}
|
||||
|
||||
let tree = repo.find_tree(id)?;
|
||||
let sig = repo.signature()?;
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
"initial",
|
||||
&tree,
|
||||
&[],
|
||||
)?;
|
||||
}
|
||||
Ok((td, repo))
|
||||
}
|
||||
/// helper returning amount of files with changes in the (wd,stage)
|
||||
pub fn get_statuses(repo_path: &RepoPath) -> (usize, usize) {
|
||||
(
|
||||
get_status(repo_path, StatusType::WorkingDir, None)
|
||||
.unwrap()
|
||||
.len(),
|
||||
get_status(repo_path, StatusType::Stage, None)
|
||||
.unwrap()
|
||||
.len(),
|
||||
)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn repo_clone(p: &str) -> Result<(TempDir, Repository)> {
|
||||
sandbox_config_files();
|
||||
///
|
||||
pub fn debug_cmd_print(path: &RepoPath, cmd: &str) {
|
||||
let cmd = debug_cmd(path, cmd);
|
||||
eprintln!("\n----\n{cmd}");
|
||||
}
|
||||
|
||||
let td = TempDir::new()?;
|
||||
/// helper to fetch commit details using log walker
|
||||
pub fn get_commit_ids(
|
||||
r: &Repository,
|
||||
max_count: usize,
|
||||
) -> Vec<CommitId> {
|
||||
let mut commit_ids = Vec::<CommitId>::new();
|
||||
LogWalker::new(r, max_count)
|
||||
.unwrap()
|
||||
.read(&mut commit_ids)
|
||||
.unwrap();
|
||||
|
||||
let td_path = td.path().as_os_str().to_str().unwrap();
|
||||
commit_ids
|
||||
}
|
||||
|
||||
let repo = Repository::clone(p, td_path).unwrap();
|
||||
/// Same as `repo_init`, but the repo is a bare repo (--bare)
|
||||
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
|
||||
init_log();
|
||||
|
||||
let mut config = repo.config()?;
|
||||
config.set_str("user.name", "name")?;
|
||||
config.set_str("user.email", "email")?;
|
||||
let tmp_repo_dir = TempDir::new()?;
|
||||
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
|
||||
Ok((tmp_repo_dir, bare_repo))
|
||||
}
|
||||
|
||||
Ok((td, repo))
|
||||
}
|
||||
/// Calling `set_search_path` with an empty directory makes sure that there
|
||||
/// is no git config interfering with our tests (for example user-local
|
||||
/// `.gitconfig`).
|
||||
#[allow(unsafe_code)]
|
||||
fn sandbox_config_files() {
|
||||
use git2::{opts::set_search_path, ConfigLevel};
|
||||
use std::sync::Once;
|
||||
|
||||
/// Same as repo_init, but the repo is a bare repo (--bare)
|
||||
pub fn repo_init_bare() -> Result<(TempDir, Repository)> {
|
||||
let tmp_repo_dir = TempDir::new()?;
|
||||
let bare_repo = Repository::init_bare(tmp_repo_dir.path())?;
|
||||
Ok((tmp_repo_dir, bare_repo))
|
||||
}
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
/// helper returning amount of files with changes in the (wd,stage)
|
||||
pub fn get_statuses(repo_path: &str) -> (usize, usize) {
|
||||
(
|
||||
get_status(repo_path, StatusType::WorkingDir)
|
||||
.unwrap()
|
||||
.len(),
|
||||
get_status(repo_path, StatusType::Stage).unwrap().len(),
|
||||
)
|
||||
}
|
||||
// Adapted from https://github.com/rust-lang/cargo/pull/9035
|
||||
INIT.call_once(|| unsafe {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let path = temp_dir.path();
|
||||
|
||||
///
|
||||
pub fn debug_cmd_print(path: &str, cmd: &str) {
|
||||
let cmd = debug_cmd(path, cmd);
|
||||
eprintln!("\n----\n{}", cmd);
|
||||
}
|
||||
set_search_path(ConfigLevel::System, path).unwrap();
|
||||
set_search_path(ConfigLevel::Global, path).unwrap();
|
||||
set_search_path(ConfigLevel::XDG, path).unwrap();
|
||||
set_search_path(ConfigLevel::ProgramData, path).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
/// helper to fetch commmit details using log walker
|
||||
pub fn get_commit_ids(
|
||||
r: &Repository,
|
||||
max_count: usize,
|
||||
) -> Vec<CommitId> {
|
||||
let mut commit_ids = Vec::<CommitId>::new();
|
||||
LogWalker::new(r, max_count)
|
||||
.unwrap()
|
||||
.read(&mut commit_ids)
|
||||
.unwrap();
|
||||
fn commit_at(
|
||||
repo_path: &RepoPath,
|
||||
msg: &str,
|
||||
time: git2::Time,
|
||||
) -> CommitId {
|
||||
let repo = repo(repo_path).unwrap();
|
||||
|
||||
commit_ids
|
||||
}
|
||||
let signature =
|
||||
git2::Signature::new("name", "email", &time).unwrap();
|
||||
let mut index = repo.index().unwrap();
|
||||
let tree_id = index.write_tree().unwrap();
|
||||
let tree = repo.find_tree(tree_id).unwrap();
|
||||
|
||||
fn debug_cmd(path: &str, cmd: &str) -> String {
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(&["/C", cmd])
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.unwrap()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.current_dir(path)
|
||||
.output()
|
||||
.unwrap()
|
||||
};
|
||||
let parents = if let Ok(id) = get_head_repo(&repo) {
|
||||
vec![repo.find_commit(id.into()).unwrap()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
format!(
|
||||
"{}{}",
|
||||
if stdout.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("out:\n{}", stdout)
|
||||
},
|
||||
if stderr.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("err:\n{}", stderr)
|
||||
}
|
||||
)
|
||||
}
|
||||
let parents = parents.iter().collect::<Vec<_>>();
|
||||
|
||||
let commit = repo
|
||||
.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
parents.as_slice(),
|
||||
)
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
commit
|
||||
}
|
||||
|
||||
// init log
|
||||
fn init_log() {
|
||||
let _ = env_logger::builder()
|
||||
.is_test(true)
|
||||
.filter_level(log::LevelFilter::Trace)
|
||||
.try_init();
|
||||
}
|
||||
|
||||
fn debug_cmd(path: &RepoPath, cmd: &str) -> String {
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", cmd])
|
||||
.current_dir(path.gitpath())
|
||||
.output()
|
||||
.unwrap()
|
||||
} else {
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.current_dir(path.gitpath())
|
||||
.output()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
format!(
|
||||
"{}{}",
|
||||
if stdout.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("out:\n{stdout}")
|
||||
},
|
||||
if stderr.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("err:\n{stderr}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,73 +1,78 @@
|
|||
use super::diff::{get_diff_raw, HunkHeader};
|
||||
use super::diff::{get_diff_raw, DiffOptions, HunkHeader};
|
||||
use crate::error::{Error, Result};
|
||||
use git2::{Diff, DiffLine, Patch, Repository};
|
||||
|
||||
#[allow(clippy::redundant_pub_crate)]
|
||||
pub(crate) struct HunkLines<'a> {
|
||||
pub hunk: HunkHeader,
|
||||
pub lines: Vec<DiffLine<'a>>,
|
||||
pub struct HunkLines<'a> {
|
||||
pub hunk: HunkHeader,
|
||||
pub lines: Vec<DiffLine<'a>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_pub_crate)]
|
||||
pub(crate) fn get_file_diff_patch_and_hunklines<'a>(
|
||||
repo: &'a Repository,
|
||||
file: &str,
|
||||
is_staged: bool,
|
||||
reverse: bool,
|
||||
) -> Result<(Patch<'a>, Vec<HunkLines<'a>>)> {
|
||||
let diff = get_diff_raw(repo, file, is_staged, reverse, Some(1))?;
|
||||
let patches = get_patches(&diff)?;
|
||||
if patches.len() > 1 {
|
||||
return Err(Error::Generic(String::from("patch error")));
|
||||
}
|
||||
pub fn get_file_diff_patch<'a>(
|
||||
repo: &'a Repository,
|
||||
file: &str,
|
||||
is_staged: bool,
|
||||
reverse: bool,
|
||||
) -> Result<Patch<'a>> {
|
||||
let diff = get_diff_raw(
|
||||
repo,
|
||||
file,
|
||||
is_staged,
|
||||
reverse,
|
||||
Some(DiffOptions {
|
||||
context: 1,
|
||||
..DiffOptions::default()
|
||||
}),
|
||||
)?;
|
||||
let patches = get_patches(&diff)?;
|
||||
if patches.len() > 1 {
|
||||
return Err(Error::Generic(String::from("patch error")));
|
||||
}
|
||||
|
||||
let patch = patches.into_iter().next().ok_or_else(|| {
|
||||
Error::Generic(String::from("no patch found"))
|
||||
})?;
|
||||
let patch = patches.into_iter().next().ok_or_else(|| {
|
||||
Error::Generic(String::from("no patch found"))
|
||||
})?;
|
||||
|
||||
let lines = patch_get_hunklines(&patch)?;
|
||||
|
||||
Ok((patch, lines))
|
||||
Ok(patch)
|
||||
}
|
||||
|
||||
//
|
||||
fn patch_get_hunklines<'a>(
|
||||
patch: &Patch<'a>,
|
||||
pub fn patch_get_hunklines<'a>(
|
||||
patch: &'a Patch<'a>,
|
||||
) -> Result<Vec<HunkLines<'a>>> {
|
||||
let count_hunks = patch.num_hunks();
|
||||
let mut res = Vec::with_capacity(count_hunks);
|
||||
for hunk_idx in 0..count_hunks {
|
||||
let (hunk, _) = patch.hunk(hunk_idx)?;
|
||||
let count_hunks = patch.num_hunks();
|
||||
let mut res = Vec::with_capacity(count_hunks);
|
||||
for hunk_idx in 0..count_hunks {
|
||||
let (hunk, _) = patch.hunk(hunk_idx)?;
|
||||
|
||||
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
|
||||
let count_lines = patch.num_lines_in_hunk(hunk_idx)?;
|
||||
|
||||
let mut hunk = HunkLines {
|
||||
hunk: HunkHeader::from(hunk),
|
||||
lines: Vec::with_capacity(count_lines),
|
||||
};
|
||||
let mut hunk = HunkLines {
|
||||
hunk: HunkHeader::from(hunk),
|
||||
lines: Vec::with_capacity(count_lines),
|
||||
};
|
||||
|
||||
for line_idx in 0..count_lines {
|
||||
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
|
||||
hunk.lines.push(line);
|
||||
}
|
||||
for line_idx in 0..count_lines {
|
||||
let line = patch.line_in_hunk(hunk_idx, line_idx)?;
|
||||
hunk.lines.push(line);
|
||||
}
|
||||
|
||||
res.push(hunk);
|
||||
}
|
||||
res.push(hunk);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
//
|
||||
fn get_patches<'a>(diff: &Diff<'a>) -> Result<Vec<Patch<'a>>> {
|
||||
let count = diff.deltas().len();
|
||||
let count = diff.deltas().len();
|
||||
|
||||
let mut res = Vec::with_capacity(count);
|
||||
for idx in 0..count {
|
||||
let p = Patch::from_diff(diff, idx)?;
|
||||
if let Some(p) = p {
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
let mut res = Vec::with_capacity(count);
|
||||
for idx in 0..count {
|
||||
let p = Patch::from_diff(diff, idx)?;
|
||||
if let Some(p) = p {
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
|||
343
asyncgit/src/sync/rebase.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
use git2::{BranchType, Repository};
|
||||
use scopetime::scope_time;
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::repository::repo,
|
||||
};
|
||||
|
||||
use super::{CommitId, RepoPath};
|
||||
|
||||
/// rebase current HEAD on `branch`
|
||||
pub fn rebase_branch(
|
||||
repo_path: &RepoPath,
|
||||
branch: &str,
|
||||
branch_type: BranchType,
|
||||
) -> Result<RebaseState> {
|
||||
scope_time!("rebase_branch");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
rebase_branch_repo(&repo, branch, branch_type)
|
||||
}
|
||||
|
||||
fn rebase_branch_repo(
|
||||
repo: &Repository,
|
||||
branch_name: &str,
|
||||
branch_type: BranchType,
|
||||
) -> Result<RebaseState> {
|
||||
let branch = repo.find_branch(branch_name, branch_type)?;
|
||||
|
||||
let annotated =
|
||||
repo.reference_to_annotated_commit(&branch.into_reference())?;
|
||||
|
||||
rebase(repo, &annotated)
|
||||
}
|
||||
|
||||
/// rebase attempt which aborts and undo's rebase if any conflict appears
|
||||
pub fn conflict_free_rebase(
|
||||
repo: &git2::Repository,
|
||||
commit: &git2::AnnotatedCommit,
|
||||
) -> Result<CommitId> {
|
||||
let mut rebase = repo.rebase(None, Some(commit), None, None)?;
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(repo)?;
|
||||
let mut last_commit = None;
|
||||
while let Some(op) = rebase.next() {
|
||||
let _op = op?;
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
rebase.abort()?;
|
||||
return Err(Error::RebaseConflict);
|
||||
}
|
||||
|
||||
let c = rebase.commit(None, &signature, None)?;
|
||||
|
||||
last_commit = Some(CommitId::from(c));
|
||||
}
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
rebase.abort()?;
|
||||
return Err(Error::RebaseConflict);
|
||||
}
|
||||
|
||||
rebase.finish(Some(&signature))?;
|
||||
|
||||
last_commit.ok_or_else(|| {
|
||||
Error::Generic(String::from("no commit rebased"))
|
||||
})
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum RebaseState {
|
||||
///
|
||||
Finished,
|
||||
///
|
||||
Conflicted,
|
||||
}
|
||||
|
||||
/// rebase
|
||||
pub fn rebase(
|
||||
repo: &git2::Repository,
|
||||
commit: &git2::AnnotatedCommit,
|
||||
) -> Result<RebaseState> {
|
||||
let mut rebase = repo.rebase(None, Some(commit), None, None)?;
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(repo)?;
|
||||
|
||||
while let Some(op) = rebase.next() {
|
||||
let _op = op?;
|
||||
// dbg!(op.id());
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Ok(RebaseState::Conflicted);
|
||||
}
|
||||
|
||||
rebase.commit(None, &signature, None)?;
|
||||
}
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Ok(RebaseState::Conflicted);
|
||||
}
|
||||
|
||||
rebase.finish(Some(&signature))?;
|
||||
|
||||
Ok(RebaseState::Finished)
|
||||
}
|
||||
|
||||
/// continue pending rebase
|
||||
pub fn continue_rebase(
|
||||
repo: &git2::Repository,
|
||||
) -> Result<RebaseState> {
|
||||
let mut rebase = repo.open_rebase(None)?;
|
||||
let signature =
|
||||
crate::sync::commit::signature_allow_undefined_name(repo)?;
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Ok(RebaseState::Conflicted);
|
||||
}
|
||||
|
||||
// try commit current rebase step
|
||||
if !repo.index()?.is_empty() {
|
||||
rebase.commit(None, &signature, None)?;
|
||||
}
|
||||
|
||||
while let Some(op) = rebase.next() {
|
||||
let _op = op?;
|
||||
// dbg!(op.id());
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Ok(RebaseState::Conflicted);
|
||||
}
|
||||
|
||||
rebase.commit(None, &signature, None)?;
|
||||
}
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Ok(RebaseState::Conflicted);
|
||||
}
|
||||
|
||||
rebase.finish(Some(&signature))?;
|
||||
|
||||
Ok(RebaseState::Finished)
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct RebaseProgress {
|
||||
///
|
||||
pub steps: usize,
|
||||
///
|
||||
pub current: usize,
|
||||
///
|
||||
pub current_commit: Option<CommitId>,
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_rebase_progress(
|
||||
repo: &git2::Repository,
|
||||
) -> Result<RebaseProgress> {
|
||||
let mut rebase = repo.open_rebase(None)?;
|
||||
|
||||
let current_commit: Option<CommitId> = rebase
|
||||
.operation_current()
|
||||
.and_then(|idx| rebase.nth(idx))
|
||||
.map(|op| op.id().into());
|
||||
|
||||
let progress = RebaseProgress {
|
||||
steps: rebase.len(),
|
||||
current: rebase.operation_current().unwrap_or_default(),
|
||||
current_commit,
|
||||
};
|
||||
|
||||
Ok(progress)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn abort_rebase(repo: &git2::Repository) -> Result<()> {
|
||||
let mut rebase = repo.open_rebase(None)?;
|
||||
|
||||
rebase.abort()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_conflict_free_rebase {
|
||||
use crate::sync::{
|
||||
checkout_branch, create_branch,
|
||||
rebase::{rebase_branch, RebaseState},
|
||||
repo_state,
|
||||
repository::repo,
|
||||
tests::{repo_init, write_commit_file},
|
||||
CommitId, RepoPath, RepoState,
|
||||
};
|
||||
use git2::{BranchType, Repository};
|
||||
|
||||
use super::conflict_free_rebase;
|
||||
|
||||
fn parent_ids(repo: &Repository, c: CommitId) -> Vec<CommitId> {
|
||||
let foo = repo
|
||||
.find_commit(c.into())
|
||||
.unwrap()
|
||||
.parent_ids()
|
||||
.map(CommitId::from)
|
||||
.collect();
|
||||
|
||||
foo
|
||||
}
|
||||
|
||||
///
|
||||
fn test_rebase_branch_repo(
|
||||
repo_path: &RepoPath,
|
||||
branch_name: &str,
|
||||
) -> CommitId {
|
||||
let repo = repo(repo_path).unwrap();
|
||||
|
||||
let branch =
|
||||
repo.find_branch(branch_name, BranchType::Local).unwrap();
|
||||
|
||||
let annotated = repo
|
||||
.reference_to_annotated_commit(&branch.into_reference())
|
||||
.unwrap();
|
||||
|
||||
conflict_free_rebase(&repo, &annotated).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
let c1 =
|
||||
write_commit_file(&repo, "test1.txt", "test", "commit1");
|
||||
|
||||
create_branch(repo_path, "foo").unwrap();
|
||||
|
||||
let c2 =
|
||||
write_commit_file(&repo, "test2.txt", "test", "commit2");
|
||||
|
||||
assert_eq!(parent_ids(&repo, c2), vec![c1]);
|
||||
|
||||
checkout_branch(repo_path, "master").unwrap();
|
||||
|
||||
let c3 =
|
||||
write_commit_file(&repo, "test3.txt", "test", "commit3");
|
||||
|
||||
checkout_branch(repo_path, "foo").unwrap();
|
||||
|
||||
let r = test_rebase_branch_repo(repo_path, "master");
|
||||
|
||||
assert_eq!(parent_ids(&repo, r), vec![c3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conflict() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test1", "commit1");
|
||||
|
||||
create_branch(repo_path, "foo").unwrap();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test2", "commit2");
|
||||
|
||||
checkout_branch(repo_path, "master").unwrap();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test3", "commit3");
|
||||
|
||||
checkout_branch(repo_path, "foo").unwrap();
|
||||
|
||||
let res =
|
||||
rebase_branch(repo_path, "master", BranchType::Local);
|
||||
|
||||
assert!(matches!(res.unwrap(), RebaseState::Conflicted));
|
||||
|
||||
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_rebase {
|
||||
use crate::sync::{
|
||||
checkout_branch, create_branch,
|
||||
rebase::{
|
||||
abort_rebase, get_rebase_progress, RebaseProgress,
|
||||
RebaseState,
|
||||
},
|
||||
rebase_branch, repo_state,
|
||||
tests::{repo_init, write_commit_file},
|
||||
RepoPath, RepoState,
|
||||
};
|
||||
use git2::BranchType;
|
||||
|
||||
#[test]
|
||||
fn test_conflicted_abort() {
|
||||
let (_td, repo) = repo_init().unwrap();
|
||||
let root = repo.path().parent().unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&root.as_os_str().to_str().unwrap().into();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test1", "commit1");
|
||||
|
||||
create_branch(repo_path, "foo").unwrap();
|
||||
|
||||
let c =
|
||||
write_commit_file(&repo, "test.txt", "test2", "commit2");
|
||||
|
||||
checkout_branch(repo_path, "master").unwrap();
|
||||
|
||||
write_commit_file(&repo, "test.txt", "test3", "commit3");
|
||||
|
||||
checkout_branch(repo_path, "foo").unwrap();
|
||||
|
||||
assert!(get_rebase_progress(&repo).is_err());
|
||||
|
||||
// rebase
|
||||
|
||||
let r = rebase_branch(repo_path, "master", BranchType::Local)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r, RebaseState::Conflicted);
|
||||
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Rebase);
|
||||
assert_eq!(
|
||||
get_rebase_progress(&repo).unwrap(),
|
||||
RebaseProgress {
|
||||
current: 0,
|
||||
steps: 1,
|
||||
current_commit: Some(c)
|
||||
}
|
||||
);
|
||||
|
||||
// abort
|
||||
|
||||
abort_rebase(&repo).unwrap();
|
||||
|
||||
assert_eq!(repo_state(repo_path).unwrap(), RepoState::Clean);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,223 +1,221 @@
|
|||
#![allow(dead_code)]
|
||||
|
||||
use super::push::ProgressNotification;
|
||||
use crate::{error::Result, sync::cred::BasicAuthCredential};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::{Cred, Error as GitError, RemoteCallbacks};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
|
||||
///
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CallbackStats {
|
||||
pub push_rejected_msg: Option<(String, String)>,
|
||||
pub push_rejected_msg: Option<(String, String)>,
|
||||
}
|
||||
|
||||
///
|
||||
#[derive(Clone)]
|
||||
pub struct Callbacks {
|
||||
sender: Option<Sender<ProgressNotification>>,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
stats: Arc<Mutex<CallbackStats>>,
|
||||
first_call_to_credentials: Arc<AtomicBool>,
|
||||
sender: Option<Sender<ProgressNotification>>,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
stats: Arc<Mutex<CallbackStats>>,
|
||||
first_call_to_credentials: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Callbacks {
|
||||
///
|
||||
pub fn new(
|
||||
sender: Option<Sender<ProgressNotification>>,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Self {
|
||||
let stats = Arc::new(Mutex::new(CallbackStats::default()));
|
||||
///
|
||||
pub fn new(
|
||||
sender: Option<Sender<ProgressNotification>>,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
) -> Self {
|
||||
let stats = Arc::new(Mutex::new(CallbackStats::default()));
|
||||
|
||||
Self {
|
||||
sender,
|
||||
basic_credential,
|
||||
stats,
|
||||
first_call_to_credentials: Arc::new(AtomicBool::new(
|
||||
true,
|
||||
)),
|
||||
}
|
||||
}
|
||||
Self {
|
||||
sender,
|
||||
basic_credential,
|
||||
stats,
|
||||
first_call_to_credentials: Arc::new(AtomicBool::new(
|
||||
true,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_stats(&self) -> Result<CallbackStats> {
|
||||
let stats = self.stats.lock()?;
|
||||
Ok(stats.clone())
|
||||
}
|
||||
///
|
||||
pub fn get_stats(&self) -> Result<CallbackStats> {
|
||||
let stats = self.stats.lock()?;
|
||||
Ok(stats.clone())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
///
|
||||
pub fn callbacks<'a>(&self) -> RemoteCallbacks<'a> {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.push_transfer_progress(
|
||||
move |current, total, bytes| {
|
||||
this.push_transfer_progress(current, total, bytes);
|
||||
},
|
||||
);
|
||||
let this = self.clone();
|
||||
callbacks.push_transfer_progress(
|
||||
move |current, total, bytes| {
|
||||
this.push_transfer_progress(current, total, bytes);
|
||||
},
|
||||
);
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.update_tips(move |name, a, b| {
|
||||
this.update_tips(name, a, b);
|
||||
true
|
||||
});
|
||||
let this = self.clone();
|
||||
callbacks.update_tips(move |name, a, b| {
|
||||
this.update_tips(name, a, b);
|
||||
true
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.transfer_progress(move |p| {
|
||||
this.transfer_progress(&p);
|
||||
true
|
||||
});
|
||||
let this = self.clone();
|
||||
callbacks.transfer_progress(move |p| {
|
||||
this.transfer_progress(&p);
|
||||
true
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.pack_progress(move |stage, current, total| {
|
||||
this.pack_progress(stage, total, current);
|
||||
});
|
||||
let this = self.clone();
|
||||
callbacks.pack_progress(move |stage, current, total| {
|
||||
this.pack_progress(stage, total, current);
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.push_update_reference(move |reference, msg| {
|
||||
this.push_update_reference(reference, msg);
|
||||
Ok(())
|
||||
});
|
||||
let this = self.clone();
|
||||
callbacks.push_update_reference(move |reference, msg| {
|
||||
this.push_update_reference(reference, msg);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
callbacks.credentials(
|
||||
move |url, username_from_url, allowed_types| {
|
||||
this.credentials(
|
||||
url,
|
||||
username_from_url,
|
||||
allowed_types,
|
||||
)
|
||||
},
|
||||
);
|
||||
let this = self.clone();
|
||||
callbacks.credentials(
|
||||
move |url, username_from_url, allowed_types| {
|
||||
this.credentials(
|
||||
url,
|
||||
username_from_url,
|
||||
allowed_types,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
callbacks
|
||||
}
|
||||
callbacks.sideband_progress(move |data| {
|
||||
log::debug!(
|
||||
"sideband transfer: '{}'",
|
||||
String::from_utf8_lossy(data).trim()
|
||||
);
|
||||
true
|
||||
});
|
||||
|
||||
fn push_update_reference(
|
||||
&self,
|
||||
reference: &str,
|
||||
msg: Option<&str>,
|
||||
) {
|
||||
log::debug!(
|
||||
"push_update_reference: '{}' {:?}",
|
||||
reference,
|
||||
msg
|
||||
);
|
||||
callbacks
|
||||
}
|
||||
|
||||
if let Ok(mut stats) = self.stats.lock() {
|
||||
stats.push_rejected_msg = msg
|
||||
.map(|msg| (reference.to_string(), msg.to_string()));
|
||||
}
|
||||
}
|
||||
fn push_update_reference(
|
||||
&self,
|
||||
reference: &str,
|
||||
msg: Option<&str>,
|
||||
) {
|
||||
log::debug!("push_update_reference: '{reference}' {msg:?}");
|
||||
|
||||
fn pack_progress(
|
||||
&self,
|
||||
stage: git2::PackBuilderStage,
|
||||
total: usize,
|
||||
current: usize,
|
||||
) {
|
||||
log::debug!("packing: {:?} - {}/{}", stage, current, total);
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::Packing {
|
||||
stage,
|
||||
total,
|
||||
current,
|
||||
})
|
||||
});
|
||||
}
|
||||
if let Ok(mut stats) = self.stats.lock() {
|
||||
stats.push_rejected_msg = msg
|
||||
.map(|msg| (reference.to_string(), msg.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_progress(&self, p: &git2::Progress) {
|
||||
log::debug!(
|
||||
"transfer: {}/{}",
|
||||
p.received_objects(),
|
||||
p.total_objects()
|
||||
);
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::Transfer {
|
||||
objects: p.received_objects(),
|
||||
total_objects: p.total_objects(),
|
||||
})
|
||||
});
|
||||
}
|
||||
fn pack_progress(
|
||||
&self,
|
||||
stage: git2::PackBuilderStage,
|
||||
total: usize,
|
||||
current: usize,
|
||||
) {
|
||||
log::debug!("packing: {stage:?} - {current}/{total}");
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::Packing {
|
||||
stage,
|
||||
total,
|
||||
current,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
|
||||
log::debug!("update tips: '{}' [{}] [{}]", name, a, b);
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::UpdateTips {
|
||||
name: name.to_string(),
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
});
|
||||
}
|
||||
fn transfer_progress(&self, p: &git2::Progress) {
|
||||
log::debug!(
|
||||
"transfer: {}/{}",
|
||||
p.received_objects(),
|
||||
p.total_objects()
|
||||
);
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::Transfer {
|
||||
objects: p.received_objects(),
|
||||
total_objects: p.total_objects(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn push_transfer_progress(
|
||||
&self,
|
||||
current: usize,
|
||||
total: usize,
|
||||
bytes: usize,
|
||||
) {
|
||||
log::debug!("progress: {}/{} ({} B)", current, total, bytes,);
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::PushTransfer {
|
||||
current,
|
||||
total,
|
||||
bytes,
|
||||
})
|
||||
});
|
||||
}
|
||||
fn update_tips(&self, name: &str, a: git2::Oid, b: git2::Oid) {
|
||||
log::debug!("update tips: '{name}' [{a}] [{b}]");
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::UpdateTips {
|
||||
name: name.to_string(),
|
||||
a: a.into(),
|
||||
b: b.into(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their creds.
|
||||
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
|
||||
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
|
||||
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
|
||||
fn credentials(
|
||||
&self,
|
||||
url: &str,
|
||||
username_from_url: Option<&str>,
|
||||
allowed_types: git2::CredentialType,
|
||||
) -> std::result::Result<Cred, GitError> {
|
||||
log::debug!(
|
||||
"creds: '{}' {:?} ({:?})",
|
||||
url,
|
||||
username_from_url,
|
||||
allowed_types
|
||||
);
|
||||
fn push_transfer_progress(
|
||||
&self,
|
||||
current: usize,
|
||||
total: usize,
|
||||
bytes: usize,
|
||||
) {
|
||||
log::debug!("progress: {current}/{total} ({bytes} B)");
|
||||
self.sender.clone().map(|sender| {
|
||||
sender.send(ProgressNotification::PushTransfer {
|
||||
current,
|
||||
total,
|
||||
bytes,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// This boolean is used to avoid multiple calls to credentials callback.
|
||||
if self.first_call_to_credentials.load(Ordering::Relaxed) {
|
||||
self.first_call_to_credentials
|
||||
.store(false, Ordering::Relaxed);
|
||||
} else {
|
||||
return Err(GitError::from_str("Bad credentials."));
|
||||
}
|
||||
// If credentials are bad, we don't ask the user to re-fill their creds. We push an error and they will be able to restart their action (for example a push) and retype their creds.
|
||||
// This behavior is explained in a issue on git2-rs project : https://github.com/rust-lang/git2-rs/issues/347
|
||||
// An implementation reference is done in cargo : https://github.com/rust-lang/cargo/blob/9fb208dddb12a3081230a5fd8f470e01df8faa25/src/cargo/sources/git/utils.rs#L588
|
||||
// There is also a guide about libgit2 authentication : https://libgit2.org/docs/guides/authentication/
|
||||
fn credentials(
|
||||
&self,
|
||||
url: &str,
|
||||
username_from_url: Option<&str>,
|
||||
allowed_types: git2::CredentialType,
|
||||
) -> std::result::Result<Cred, GitError> {
|
||||
log::debug!(
|
||||
"creds: '{url}' {username_from_url:?} ({allowed_types:?})",
|
||||
);
|
||||
|
||||
match &self.basic_credential {
|
||||
_ if allowed_types.is_ssh_key() => {
|
||||
match username_from_url {
|
||||
Some(username) => {
|
||||
Cred::ssh_key_from_agent(username)
|
||||
}
|
||||
None => Err(GitError::from_str(
|
||||
" Couldn't extract username from url.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: Some(pwd),
|
||||
}) if allowed_types.is_user_pass_plaintext() => {
|
||||
Cred::userpass_plaintext(user, pwd)
|
||||
}
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: _,
|
||||
}) if allowed_types.is_username() => Cred::username(user),
|
||||
_ if allowed_types.is_default() => Cred::default(),
|
||||
_ => Err(GitError::from_str("Couldn't find credentials")),
|
||||
}
|
||||
}
|
||||
// This boolean is used to avoid multiple calls to credentials callback.
|
||||
if self.first_call_to_credentials.load(Ordering::Relaxed) {
|
||||
self.first_call_to_credentials
|
||||
.store(false, Ordering::Relaxed);
|
||||
} else {
|
||||
return Err(GitError::from_str("Bad credentials."));
|
||||
}
|
||||
|
||||
match &self.basic_credential {
|
||||
_ if allowed_types.is_ssh_key() => username_from_url
|
||||
.map_or_else(
|
||||
|| {
|
||||
Err(GitError::from_str(
|
||||
" Couldn't extract username from url.",
|
||||
))
|
||||
},
|
||||
Cred::ssh_key_from_agent,
|
||||
),
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: Some(pwd),
|
||||
}) if allowed_types.is_user_pass_plaintext() => {
|
||||
Cred::userpass_plaintext(user, pwd)
|
||||
}
|
||||
Some(BasicAuthCredential {
|
||||
username: Some(user),
|
||||
password: _,
|
||||
}) if allowed_types.is_username() => Cred::username(user),
|
||||
_ if allowed_types.is_default() => Cred::default(),
|
||||
_ => Err(GitError::from_str("Couldn't find credentials")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,212 +5,555 @@ pub(crate) mod push;
|
|||
pub(crate) mod tags;
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::push::ProgressNotification, utils,
|
||||
},
|
||||
error::{Error, Result},
|
||||
sync::{
|
||||
cred::BasicAuthCredential,
|
||||
remotes::push::ProgressNotification, repository::repo, utils,
|
||||
},
|
||||
ProgressPercent,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::{BranchType, FetchOptions, Repository};
|
||||
use git2::{
|
||||
BranchType, FetchOptions, ProxyOptions, Remote, Repository,
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
use utils::bytes2string;
|
||||
|
||||
pub use callbacks::Callbacks;
|
||||
pub use tags::tags_missing_remote;
|
||||
|
||||
use super::RepoPath;
|
||||
|
||||
/// origin
|
||||
pub const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||
|
||||
///
|
||||
pub fn get_remotes(repo_path: &str) -> Result<Vec<String>> {
|
||||
scope_time!("get_remotes");
|
||||
pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
|
||||
let mut proxy = ProxyOptions::new();
|
||||
proxy.auto();
|
||||
proxy
|
||||
}
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let remotes = repo.remotes()?;
|
||||
let remotes: Vec<String> =
|
||||
remotes.iter().flatten().map(String::from).collect();
|
||||
///
|
||||
pub fn add_remote(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
url: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote(name, url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(remotes)
|
||||
///
|
||||
pub fn rename_remote(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_rename(name, new_name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_remote_url(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
new_url: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_set_url(name, new_url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn delete_remote(
|
||||
repo_path: &RepoPath,
|
||||
remote_name: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_delete(remote_name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn validate_remote_name(name: &str) -> bool {
|
||||
Remote::is_valid_name(name)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
|
||||
scope_time!("get_remotes");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let remotes = repo.remotes()?;
|
||||
let remotes: Vec<String> =
|
||||
remotes.iter().flatten().map(String::from).collect();
|
||||
|
||||
Ok(remotes)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_remote_url(
|
||||
repo_path: &RepoPath,
|
||||
remote_name: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let repo = repo(repo_path)?;
|
||||
let remote = repo.find_remote(remote_name)?.clone();
|
||||
let url = remote.url();
|
||||
if let Some(u) = url {
|
||||
return Ok(Some(u.to_string()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// tries to find origin or the only remote that is defined if any
|
||||
/// in case of multiple remotes and none named *origin* we fail
|
||||
pub fn get_default_remote(repo_path: &str) -> Result<String> {
|
||||
let repo = utils::repo(repo_path)?;
|
||||
get_default_remote_in_repo(&repo)
|
||||
pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
|
||||
let repo = repo(repo_path)?;
|
||||
get_default_remote_in_repo(&repo)
|
||||
}
|
||||
|
||||
/// Gets the current branch the user is on.
|
||||
/// Returns none if they are not on a branch
|
||||
/// and Err if there was a problem finding the branch
|
||||
fn get_current_branch(
|
||||
repo: &Repository,
|
||||
) -> Result<Option<git2::Branch<'_>>> {
|
||||
for b in repo.branches(None)? {
|
||||
let branch = b?.0;
|
||||
if branch.is_head() {
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Tries to find the default repo to fetch from based on configuration.
|
||||
///
|
||||
/// > `branch.<name>.remote`
|
||||
/// >
|
||||
/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or
|
||||
/// > push to. [...] If no remote is configured, or if you are not on any branch and there is more
|
||||
/// > than one remote defined in the repository, it defaults to `origin` for fetching [...].
|
||||
///
|
||||
/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
|
||||
///
|
||||
/// Falls back to `get_default_remote_in_repo`.
|
||||
pub fn get_default_remote_for_fetch(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<String> {
|
||||
let repo = repo(repo_path)?;
|
||||
get_default_remote_for_fetch_in_repo(&repo)
|
||||
}
|
||||
|
||||
// TODO: Very similar to `get_default_remote_for_push_in_repo`. Can probably be refactored.
|
||||
pub(crate) fn get_default_remote_for_fetch_in_repo(
|
||||
repo: &Repository,
|
||||
) -> Result<String> {
|
||||
scope_time!("get_default_remote_for_fetch_in_repo");
|
||||
|
||||
let config = repo.config()?;
|
||||
|
||||
let branch = get_current_branch(repo)?;
|
||||
|
||||
if let Some(branch) = branch {
|
||||
let remote_name = bytes2string(branch.name_bytes()?)?;
|
||||
|
||||
let entry_name = format!("branch.{}.remote", &remote_name);
|
||||
|
||||
if let Ok(entry) = config.get_entry(&entry_name) {
|
||||
return bytes2string(entry.value_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
get_default_remote_in_repo(repo)
|
||||
}
|
||||
|
||||
/// Tries to find the default repo to push to based on configuration.
|
||||
///
|
||||
/// > `remote.pushDefault`
|
||||
/// >
|
||||
/// > The remote to push to by default. Overrides `branch.<name>.remote` for all branches, and is
|
||||
/// > overridden by `branch.<name>.pushRemote` for specific branches.
|
||||
///
|
||||
/// > `branch.<name>.remote`
|
||||
/// >
|
||||
/// > When on branch `<name>`, it tells `git fetch` and `git push` which remote to fetch from or
|
||||
/// > push to. The remote to push to may be overridden with `remote.pushDefault` (for all
|
||||
/// > branches). The remote to push to, for the current branch, may be further overridden by
|
||||
/// > `branch.<name>.pushRemote`. If no remote is configured, or if you are not on any branch and
|
||||
/// > there is more than one remote defined in the repository, it defaults to `origin` for fetching
|
||||
/// > and `remote.pushDefault` for pushing.
|
||||
///
|
||||
/// [git-config-remote-push-default]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-remotepushDefault
|
||||
/// [git-config-branch-name-remote]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-branchltnamegtremote
|
||||
///
|
||||
/// Falls back to `get_default_remote_in_repo`.
|
||||
pub fn get_default_remote_for_push(
|
||||
repo_path: &RepoPath,
|
||||
) -> Result<String> {
|
||||
let repo = repo(repo_path)?;
|
||||
get_default_remote_for_push_in_repo(&repo)
|
||||
}
|
||||
|
||||
// TODO: Very similar to `get_default_remote_for_fetch_in_repo`. Can probably be refactored.
|
||||
pub(crate) fn get_default_remote_for_push_in_repo(
|
||||
repo: &Repository,
|
||||
) -> Result<String> {
|
||||
scope_time!("get_default_remote_for_push_in_repo");
|
||||
|
||||
let config = repo.config()?;
|
||||
|
||||
let branch = get_current_branch(repo)?;
|
||||
|
||||
if let Some(branch) = branch {
|
||||
let remote_name = bytes2string(branch.name_bytes()?)?;
|
||||
|
||||
let entry_name =
|
||||
format!("branch.{}.pushRemote", &remote_name);
|
||||
|
||||
if let Ok(entry) = config.get_entry(&entry_name) {
|
||||
return bytes2string(entry.value_bytes());
|
||||
}
|
||||
|
||||
if let Ok(entry) = config.get_entry("remote.pushDefault") {
|
||||
return bytes2string(entry.value_bytes());
|
||||
}
|
||||
|
||||
let entry_name = format!("branch.{}.remote", &remote_name);
|
||||
|
||||
if let Ok(entry) = config.get_entry(&entry_name) {
|
||||
return bytes2string(entry.value_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
get_default_remote_in_repo(repo)
|
||||
}
|
||||
|
||||
/// see `get_default_remote`
|
||||
pub(crate) fn get_default_remote_in_repo(
|
||||
repo: &Repository,
|
||||
repo: &Repository,
|
||||
) -> Result<String> {
|
||||
scope_time!("get_default_remote_in_repo");
|
||||
scope_time!("get_default_remote_in_repo");
|
||||
|
||||
let remotes = repo.remotes()?;
|
||||
let remotes = repo.remotes()?;
|
||||
|
||||
// if `origin` exists return that
|
||||
let found_origin = remotes.iter().any(|r| {
|
||||
r.map(|r| r == DEFAULT_REMOTE_NAME).unwrap_or_default()
|
||||
});
|
||||
if found_origin {
|
||||
return Ok(DEFAULT_REMOTE_NAME.into());
|
||||
}
|
||||
// if `origin` exists return that
|
||||
let found_origin = remotes
|
||||
.iter()
|
||||
.any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
|
||||
if found_origin {
|
||||
return Ok(DEFAULT_REMOTE_NAME.into());
|
||||
}
|
||||
|
||||
//if only one remote exists pick that
|
||||
if remotes.len() == 1 {
|
||||
let first_remote = remotes
|
||||
.iter()
|
||||
.next()
|
||||
.flatten()
|
||||
.map(String::from)
|
||||
.ok_or_else(|| {
|
||||
Error::Generic("no remote found".into())
|
||||
})?;
|
||||
//if only one remote exists pick that
|
||||
if remotes.len() == 1 {
|
||||
let first_remote = remotes
|
||||
.iter()
|
||||
.next()
|
||||
.flatten()
|
||||
.map(String::from)
|
||||
.ok_or_else(|| {
|
||||
Error::Generic("no remote found".into())
|
||||
})?;
|
||||
|
||||
return Ok(first_remote);
|
||||
}
|
||||
return Ok(first_remote);
|
||||
}
|
||||
|
||||
//inconclusive
|
||||
Err(Error::NoDefaultRemoteFound)
|
||||
//inconclusive
|
||||
Err(Error::NoDefaultRemoteFound)
|
||||
}
|
||||
|
||||
/// fetches from upstream/remote for `branch`
|
||||
///
|
||||
fn fetch_from_remote(
|
||||
repo_path: &RepoPath,
|
||||
remote: &str,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
progress_sender: Option<Sender<ProgressNotification>>,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
|
||||
let mut remote = repo.find_remote(remote)?;
|
||||
|
||||
let mut options = FetchOptions::new();
|
||||
let callbacks = Callbacks::new(progress_sender, basic_credential);
|
||||
options.prune(git2::FetchPrune::On);
|
||||
options.proxy_options(proxy_auto());
|
||||
options.download_tags(git2::AutotagOption::All);
|
||||
options.remote_callbacks(callbacks.callbacks());
|
||||
remote.fetch(&[] as &[&str], Some(&mut options), None)?;
|
||||
// fetch tags (also removing remotely deleted ones)
|
||||
remote.fetch(
|
||||
&["refs/tags/*:refs/tags/*"],
|
||||
Some(&mut options),
|
||||
None,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// updates/prunes all branches from all remotes
|
||||
pub fn fetch_all(
|
||||
repo_path: &RepoPath,
|
||||
basic_credential: &Option<BasicAuthCredential>,
|
||||
progress_sender: &Option<Sender<ProgressPercent>>,
|
||||
) -> Result<()> {
|
||||
scope_time!("fetch_all");
|
||||
|
||||
let repo = repo(repo_path)?;
|
||||
let remotes = repo
|
||||
.remotes()?
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
let remotes_count = remotes.len();
|
||||
|
||||
for (idx, remote) in remotes.into_iter().enumerate() {
|
||||
fetch_from_remote(
|
||||
repo_path,
|
||||
&remote,
|
||||
basic_credential.clone(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
if let Some(sender) = progress_sender {
|
||||
let progress = ProgressPercent::new(idx, remotes_count);
|
||||
sender.send(progress)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// fetches from upstream/remote for local `branch`
|
||||
pub(crate) fn fetch(
|
||||
repo_path: &str,
|
||||
branch: &str,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
progress_sender: Option<Sender<ProgressNotification>>,
|
||||
repo_path: &RepoPath,
|
||||
branch: &str,
|
||||
basic_credential: Option<BasicAuthCredential>,
|
||||
progress_sender: Option<Sender<ProgressNotification>>,
|
||||
) -> Result<usize> {
|
||||
scope_time!("fetch_origin");
|
||||
scope_time!("fetch");
|
||||
|
||||
let repo = utils::repo(repo_path)?;
|
||||
let branch_ref = repo
|
||||
.find_branch(branch, BranchType::Local)?
|
||||
.into_reference();
|
||||
let branch_ref = bytes2string(branch_ref.name_bytes())?;
|
||||
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
|
||||
let remote_name = bytes2string(&*remote_name)?;
|
||||
let mut remote = repo.find_remote(&remote_name)?;
|
||||
let repo = repo(repo_path)?;
|
||||
let branch_ref = repo
|
||||
.find_branch(branch, BranchType::Local)?
|
||||
.into_reference();
|
||||
let branch_ref = bytes2string(branch_ref.name_bytes())?;
|
||||
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
|
||||
let remote_name = bytes2string(&remote_name)?;
|
||||
let mut remote = repo.find_remote(&remote_name)?;
|
||||
|
||||
let mut options = FetchOptions::new();
|
||||
let callbacks = Callbacks::new(progress_sender, basic_credential);
|
||||
options.remote_callbacks(callbacks.callbacks());
|
||||
let mut options = FetchOptions::new();
|
||||
options.download_tags(git2::AutotagOption::All);
|
||||
let callbacks = Callbacks::new(progress_sender, basic_credential);
|
||||
options.remote_callbacks(callbacks.callbacks());
|
||||
options.proxy_options(proxy_auto());
|
||||
|
||||
remote.fetch(&[branch], Some(&mut options), None)?;
|
||||
remote.fetch(&[branch], Some(&mut options), None)?;
|
||||
|
||||
Ok(remote.stats().received_bytes())
|
||||
Ok(remote.stats().received_bytes())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::tests::{
|
||||
debug_cmd_print, repo_clone, repo_init,
|
||||
};
|
||||
use super::*;
|
||||
use crate::sync::tests::{
|
||||
debug_cmd_print, repo_clone, repo_init,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_smoke() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
|
||||
assert_eq!(remotes, vec![String::from("origin")]);
|
||||
assert_eq!(remotes, vec![String::from("origin")]);
|
||||
|
||||
fetch(repo_path, "master", None, None).unwrap();
|
||||
}
|
||||
fetch(repo_path, "master", None, None).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_remote() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_default_remote() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add second {}", remote_path)[..],
|
||||
);
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add second {remote_path}")[..],
|
||||
);
|
||||
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![String::from("origin"), String::from("second")]
|
||||
);
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![String::from("origin"), String::from("second")]
|
||||
);
|
||||
|
||||
let first = get_default_remote_in_repo(
|
||||
&utils::repo(repo_path).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(first, String::from("origin"));
|
||||
}
|
||||
let first =
|
||||
get_default_remote_in_repo(&repo(repo_path).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(first, String::from("origin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_remote_out_of_order() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_default_remote_out_of_order() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add origin {}", remote_path)[..],
|
||||
);
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add origin {remote_path}")[..],
|
||||
);
|
||||
|
||||
//NOTE: aparently remotes are not chronolically sorted but alphabetically
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
//NOTE: apparently remotes are not chronolically sorted but alphabetically
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![String::from("alternate"), String::from("origin")]
|
||||
);
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![String::from("alternate"), String::from("origin")]
|
||||
);
|
||||
|
||||
let first = get_default_remote_in_repo(
|
||||
&utils::repo(repo_path).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(first, String::from("origin"));
|
||||
}
|
||||
let first =
|
||||
get_default_remote_in_repo(&repo(repo_path).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(first, String::from("origin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_remote_inconclusive() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path = repo_dir.path().as_os_str().to_str().unwrap();
|
||||
#[test]
|
||||
fn test_default_remote_inconclusive() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, _repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add someremote {}", remote_path)[..],
|
||||
);
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add someremote {remote_path}")[..],
|
||||
);
|
||||
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![
|
||||
String::from("alternate"),
|
||||
String::from("someremote")
|
||||
]
|
||||
);
|
||||
let remotes = get_remotes(repo_path).unwrap();
|
||||
assert_eq!(
|
||||
remotes,
|
||||
vec![
|
||||
String::from("alternate"),
|
||||
String::from("someremote")
|
||||
]
|
||||
);
|
||||
|
||||
let res = get_default_remote_in_repo(
|
||||
&utils::repo(repo_path).unwrap(),
|
||||
);
|
||||
assert_eq!(res.is_err(), true);
|
||||
assert!(matches!(res, Err(Error::NoDefaultRemoteFound)));
|
||||
}
|
||||
let default_remote =
|
||||
get_default_remote_in_repo(&repo(repo_path).unwrap());
|
||||
|
||||
assert!(matches!(
|
||||
default_remote,
|
||||
Err(Error::NoDefaultRemoteFound)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_remote_for_fetch() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add someremote {remote_path}")[..],
|
||||
);
|
||||
|
||||
let mut config = repo.config().unwrap();
|
||||
|
||||
config
|
||||
.set_str("branch.master.remote", "branchremote")
|
||||
.unwrap();
|
||||
|
||||
let default_fetch_remote =
|
||||
get_default_remote_for_fetch_in_repo(&repo);
|
||||
|
||||
assert!(
|
||||
matches!(default_fetch_remote, Ok(remote_name) if remote_name == "branchremote")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_remote_for_push() {
|
||||
let (remote_dir, _remote) = repo_init().unwrap();
|
||||
let remote_path = remote_dir.path().to_str().unwrap();
|
||||
let (repo_dir, repo) = repo_clone(remote_path).unwrap();
|
||||
let repo_path: &RepoPath =
|
||||
&repo_dir.keep().as_os_str().to_str().unwrap().into();
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
"git remote rename origin alternate",
|
||||
);
|
||||
|
||||
debug_cmd_print(
|
||||
repo_path,
|
||||
&format!("git remote add someremote {remote_path}")[..],
|
||||
);
|
||||
|
||||
let mut config = repo.config().unwrap();
|
||||
|
||||
config
|
||||
.set_str("branch.master.remote", "branchremote")
|
||||
.unwrap();
|
||||
|
||||
let default_push_remote =
|
||||
get_default_remote_for_push_in_repo(&repo);
|
||||
|
||||
assert!(
|
||||
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchremote")
|
||||
);
|
||||
|
||||
config.set_str("remote.pushDefault", "pushdefault").unwrap();
|
||||
|
||||
let default_push_remote =
|
||||
get_default_remote_for_push_in_repo(&repo);
|
||||
|
||||
assert!(
|
||||
matches!(default_push_remote, Ok(remote_name) if remote_name == "pushdefault")
|
||||
);
|
||||
|
||||
config
|
||||
.set_str("branch.master.pushRemote", "branchpushremote")
|
||||
.unwrap();
|
||||
|
||||
let default_push_remote =
|
||||
get_default_remote_for_push_in_repo(&repo);
|
||||
|
||||
assert!(
|
||||
matches!(default_push_remote, Ok(remote_name) if remote_name == "branchpushremote")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||