mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-18 08:34:41 +04:00
Compare commits
1605 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8416413a0e | ||
|
|
37a879e65e | ||
|
|
8a0630ca1a | ||
|
|
fcbc745744 | ||
|
|
51592eecec | ||
|
|
67e8faf80b | ||
|
|
c251e6b6d0 | ||
|
|
98ccafa3ce | ||
|
|
48953556d0 | ||
|
|
2cedd58e69 | ||
|
|
465192d7de | ||
|
|
fd42f96874 | ||
|
|
8ea2b790d8 | ||
|
|
1af6c8fa95 | ||
|
|
3358a3b540 | ||
|
|
bdf68790b7 | ||
|
|
e436690f14 | ||
|
|
8d34f95308 | ||
|
|
9c7ea88ab9 | ||
|
|
844dacc168 | ||
|
|
6a1d543e52 | ||
|
|
66278a9a0b | ||
|
|
1b32c50bc7 | ||
|
|
063229aee7 | ||
|
|
49e28eea48 | ||
|
|
d5c273a2ba | ||
|
|
17cf886473 | ||
|
|
cb63a5cb9a | ||
|
|
f80801d75a | ||
|
|
9143fc3545 | ||
|
|
725acc969a | ||
|
|
3815e2e6d8 | ||
|
|
aa2961c122 | ||
|
|
46297f7cd6 | ||
|
|
95eec75004 | ||
|
|
5e4b3546f3 | ||
|
|
8bbfcf1f82 | ||
|
|
65175f3cf3 | ||
|
|
fc33a62f3c | ||
|
|
2db924eb98 | ||
|
|
ddfcf4034b | ||
|
|
1b424ccfa2 | ||
|
|
221637b0cb | ||
|
|
7ad23528d0 | ||
|
|
f415f6c4d7 | ||
|
|
99fa9ba5ef | ||
|
|
ad0d4cd427 | ||
|
|
924680e974 | ||
|
|
2930ea0fcd | ||
|
|
b96127c485 | ||
|
|
954cc3b3b3 | ||
|
|
e671830402 | ||
|
|
c78f248c60 | ||
|
|
e70b313c80 | ||
|
|
87a681f866 | ||
|
|
b74a22d96c | ||
|
|
cc899de192 | ||
|
|
2a0bcea757 | ||
|
|
f1cc7a445f | ||
|
|
72a3b71f0e | ||
|
|
c8925a457a | ||
|
|
a5fe6c500c | ||
|
|
6f1f433a9c | ||
|
|
c6817210b1 | ||
|
|
4fd4537bc6 | ||
|
|
a1d7bed86f | ||
|
|
d1d5b3d564 | ||
|
|
9c228a0bd8 | ||
|
|
d4f0c6e8a7 | ||
|
|
5f0c773e74 | ||
|
|
43b426a501 | ||
|
|
8a4af027a1 | ||
|
|
466f6c4a39 | ||
|
|
d2b61d0ef2 | ||
|
|
43592c2a29 | ||
|
|
6d47b8d52b | ||
|
|
aa48766b88 | ||
|
|
bf6f11dc6c | ||
|
|
3e8d0f5e1c | ||
|
|
ff7256cedf | ||
|
|
b7eb412c79 | ||
|
|
7b88c63ae6 | ||
|
|
94b97e28f7 | ||
|
|
2f93b80484 | ||
|
|
4f90a1e88a | ||
|
|
aba1ce04e7 | ||
|
|
e2210c97e2 | ||
|
|
804c853d8f | ||
|
|
edd0002e43 | ||
|
|
b152e922ef | ||
|
|
c3125a4d36 | ||
|
|
c2c061cf63 | ||
|
|
cd429db5e2 | ||
|
|
9b9f8e254d | ||
|
|
86300a4a7e | ||
|
|
77edad8455 | ||
|
|
18b393905e | ||
|
|
31cb945ffb | ||
|
|
28c3d81bd2 | ||
|
|
aa85be9340 | ||
|
|
1027596280 | ||
|
|
db85eeed2d | ||
|
|
cb14bc2d9c | ||
|
|
109628c505 | ||
|
|
74233a125a | ||
|
|
85783bdc0b | ||
|
|
9da45a686a | ||
|
|
708ca3b581 | ||
|
|
611195a088 | ||
|
|
0de4c8028a | ||
|
|
46de4da34e | ||
|
|
cc4a522415 | ||
|
|
772d7d45d3 | ||
|
|
222c62bc5d | ||
|
|
3d5548b487 | ||
|
|
4235a877f3 | ||
|
|
dbccd57db0 | ||
|
|
d07e52901c | ||
|
|
801b956304 | ||
|
|
a2f7b80e89 | ||
|
|
d31722386e | ||
|
|
7555e11098 | ||
|
|
05e365fe08 | ||
|
|
1d2f1c0cce | ||
|
|
33e020f315 | ||
|
|
e7cfc0d2cb | ||
|
|
7441a62eb1 | ||
|
|
eec11bd124 | ||
|
|
fa08d1d637 | ||
|
|
6c068a78d6 | ||
|
|
c9454e2ec3 | ||
|
|
e687548842 | ||
|
|
a4e4cef335 | ||
|
|
de8590064e | ||
|
|
5679399d90 | ||
|
|
a7b671a758 | ||
|
|
364e6d77f0 | ||
|
|
504149d2ca | ||
|
|
c2c68d985c | ||
|
|
0f8f383d53 | ||
|
|
d5c21926ab | ||
|
|
8a3ee454a2 | ||
|
|
917e2102be | ||
|
|
9d4ab670d1 | ||
|
|
d46423ffe1 | ||
|
|
8133abd730 | ||
|
|
da0c1d0cfa | ||
|
|
f641f6a157 | ||
|
|
a68a19d217 | ||
|
|
88317ddb34 | ||
|
|
faa6f8c70c | ||
|
|
f632459bc6 | ||
|
|
0252908c27 | ||
|
|
69b5dce12a | ||
|
|
d56502acab | ||
|
|
49a9664cd0 | ||
|
|
60db5d78d1 | ||
|
|
9dd247898c | ||
|
|
15d9f7f61e | ||
|
|
1f128556e4 | ||
|
|
dd144c95f6 | ||
|
|
5526bb2137 | ||
|
|
9ee0768353 | ||
|
|
eeac061b31 | ||
|
|
296f59266e | ||
|
|
16023286d2 | ||
|
|
0142f45322 | ||
|
|
d7e21cd29c | ||
|
|
5c3d006443 | ||
|
|
0d4c6da36e | ||
|
|
85fe18253f | ||
|
|
74d05b444d | ||
|
|
a9f19f4cc0 | ||
|
|
4e121ef25c | ||
|
|
7b4667e9e6 | ||
|
|
d46c8fc051 | ||
|
|
fc622159a6 | ||
|
|
716da1e49c | ||
|
|
0939c11b4f | ||
|
|
3bdf9fb91b | ||
|
|
75b3e52e57 | ||
|
|
5e11c266ae | ||
|
|
e58009684c | ||
|
|
7b868c3508 | ||
|
|
a768c038e3 | ||
|
|
e525726855 | ||
|
|
3187538d2b | ||
|
|
61d9c95cc1 | ||
|
|
15bd0c7993 | ||
|
|
92678c3fa2 | ||
|
|
032d8e2704 | ||
|
|
73848383f2 | ||
|
|
96e0712f43 | ||
|
|
3388e2dbdf | ||
|
|
10f8853631 | ||
|
|
9682f93e31 | ||
|
|
5f8ebaefa9 | ||
|
|
9cce283422 | ||
|
|
9c9983dba0 | ||
|
|
7dc0a59ee5 | ||
|
|
d3fa7f415c | ||
|
|
08478ec176 | ||
|
|
047439abff | ||
|
|
23a2b7360e | ||
|
|
cffabe54be | ||
|
|
51df34e5fb | ||
|
|
e36cefc8bd | ||
|
|
2280645d0e | ||
|
|
1d474994e0 | ||
|
|
14169eb31d | ||
|
|
75f2710bd4 | ||
|
|
762e68173b | ||
|
|
7f15dc75ee | ||
|
|
5f4ed54cfe | ||
|
|
c69e32e925 | ||
|
|
48f0dc9615 | ||
|
|
44b625ebd3 | ||
|
|
61cc55a59c | ||
|
|
c9945b9aa3 | ||
|
|
d988767b41 | ||
|
|
0d8b2451ca | ||
|
|
155c521348 | ||
|
|
676e51aa5e | ||
|
|
997ea0ad52 | ||
|
|
19aa121db0 | ||
|
|
a4ab977cc3 | ||
|
|
3f3760b86c | ||
|
|
051f4e2550 | ||
|
|
c68c6b944a | ||
|
|
5e05b01215 | ||
|
|
84f91646bb | ||
|
|
d33801e781 | ||
|
|
f56b75dd77 | ||
|
|
5d692d7e06 | ||
|
|
0845e4e816 | ||
|
|
5f4b828999 | ||
|
|
9ac229dad8 | ||
|
|
493c20ce91 | ||
|
|
e2c6a822c7 | ||
|
|
818240e425 | ||
|
|
fe9ba6b53f | ||
|
|
ac42dc586a | ||
|
|
ea8244b229 | ||
|
|
9ebda91889 | ||
|
|
3dd1875df8 | ||
|
|
bde292b510 | ||
|
|
a4ae868633 | ||
|
|
0c3d605fa0 | ||
|
|
5a4b5b1f8e | ||
|
|
a1f1d19156 | ||
|
|
93b1a14402 | ||
|
|
23df082edc | ||
|
|
1803db979f | ||
|
|
b2ff5e401b | ||
|
|
8c75a26f2d | ||
|
|
13b673ce3c | ||
|
|
3557077bbc | ||
|
|
2bffc0ad85 | ||
|
|
60273ca81e | ||
|
|
f8488e36c8 | ||
|
|
d537c09032 | ||
|
|
9660d3b451 | ||
|
|
5e4af63e85 | ||
|
|
4f73b8a2af | ||
|
|
9bf6856db3 | ||
|
|
8bf22bd3c1 | ||
|
|
1c439b5a79 | ||
|
|
3126802bf4 | ||
|
|
e72699b9ef | ||
|
|
8891089cf5 | ||
|
|
854f698531 | ||
|
|
c25ea47cf7 | ||
|
|
f94744a699 | ||
|
|
5da4258b17 | ||
|
|
e891f8606e | ||
|
|
50b1511a5b | ||
|
|
9a7ed8be09 | ||
|
|
f073f8e13c | ||
|
|
1fc28903ab | ||
|
|
c7f472d176 | ||
|
|
5d273951e0 | ||
|
|
f88cc61256 | ||
|
|
c716de12f7 | ||
|
|
f4a2d52bb4 | ||
|
|
8034430539 | ||
|
|
18348f5d0d | ||
|
|
a0a734750e | ||
|
|
bcd8274e1c | ||
|
|
cd09c3525e | ||
|
|
c389ac0ba8 | ||
|
|
64985bd63d | ||
|
|
f5a98eb152 | ||
|
|
08b670ff9e | ||
|
|
72b7b53ae3 | ||
|
|
9c2a01186b | ||
|
|
ee49d4289f | ||
|
|
926071b6a7 | ||
|
|
0deb445a73 | ||
|
|
8923fa368a | ||
|
|
cf3e826070 | ||
|
|
1dd85c904e | ||
|
|
51104fa94a | ||
|
|
0fd2501503 | ||
|
|
420a15f87e | ||
|
|
4c0623963f | ||
|
|
5df9eb9d66 | ||
|
|
ddbee32ad0 | ||
|
|
bab5f70a38 | ||
|
|
4f045cbc21 | ||
|
|
27d77fc8bc | ||
|
|
e885f2e76e | ||
|
|
148df0d718 | ||
|
|
4727922b93 | ||
|
|
5cd99d81ad | ||
|
|
8656797c78 | ||
|
|
6f941298a4 | ||
|
|
cfcb88364e | ||
|
|
6896025288 | ||
|
|
f5a716f1b1 | ||
|
|
5027592625 | ||
|
|
597bf7caf8 | ||
|
|
84f040f58a | ||
|
|
bb49560dc9 | ||
|
|
0477abd3ce | ||
|
|
ee71a9345c | ||
|
|
df2cada973 | ||
|
|
fdb9ede460 | ||
|
|
a412e5492d | ||
|
|
d5f919e72f | ||
|
|
7a7772fcda | ||
|
|
4caea677c6 | ||
|
|
d3c0a66d23 | ||
|
|
950451e1ef | ||
|
|
f39154f69f | ||
|
|
f2661bfc31 | ||
|
|
e3e17a1617 | ||
|
|
02e9680a46 | ||
|
|
8ab9747b33 | ||
|
|
321a90e0a1 | ||
|
|
63a180a935 | ||
|
|
fe73f519c7 | ||
|
|
0616c0abf7 | ||
|
|
321685ee0e | ||
|
|
5ce4a549d3 | ||
|
|
2822de9aa6 | ||
|
|
7f386a752e | ||
|
|
2700a7d622 | ||
|
|
294590fd12 | ||
|
|
7bfa7e15c6 | ||
|
|
a631abdabf | ||
|
|
cdd360645a | ||
|
|
4ded56efac | ||
|
|
192be6079b | ||
|
|
cddc9cff0f | ||
|
|
8ba9bc6d89 | ||
|
|
08fa5ffc43 | ||
|
|
9655158d93 | ||
|
|
1054afd978 | ||
|
|
d23990cc8b | ||
|
|
51b14c2b37 | ||
|
|
4666b6956f | ||
|
|
8d84a7a270 | ||
|
|
ccbf15150f | ||
|
|
25b8ad7528 | ||
|
|
0cc2856883 | ||
|
|
4f6ba18cad | ||
|
|
c3402e6648 | ||
|
|
c3613b954a | ||
|
|
d3883ce7f7 | ||
|
|
5bc56889e9 | ||
|
|
a2373d0a81 | ||
|
|
f5ec578647 | ||
|
|
8db5aaf4f3 | ||
|
|
8afe5958e6 | ||
|
|
ea2569027b | ||
|
|
8f2dfce5d1 | ||
|
|
4fdfb25a52 | ||
|
|
4bd1677cda | ||
|
|
16869444ca | ||
|
|
5635079373 | ||
|
|
c5fc8627be | ||
|
|
abbfb2e9b9 | ||
|
|
4d970de329 | ||
|
|
915b930a5d | ||
|
|
97c7870f89 | ||
|
|
6804100701 | ||
|
|
f7df13f3d8 | ||
|
|
0afb95ccca | ||
|
|
b609368ca5 | ||
|
|
0c1267b214 | ||
|
|
0359fae2da | ||
|
|
cc7bf965eb | ||
|
|
7783a3e63a | ||
|
|
e846829992 | ||
|
|
bfe1a72734 | ||
|
|
1129e7d222 | ||
|
|
7260d24d0f | ||
|
|
9868b1a347 | ||
|
|
d216cdd5c1 | ||
|
|
c2e432cdf6 | ||
|
|
909dd0e8a1 | ||
|
|
3c2ca11332 | ||
|
|
2530913fad | ||
|
|
650afe4ffa | ||
|
|
f54386203e | ||
|
|
50cf02764c | ||
|
|
5b50acbd2c | ||
|
|
77b4ea38ca | ||
|
|
6abb05a60c | ||
|
|
2adca78c6f | ||
|
|
2e1cfa8f05 | ||
|
|
420902f67c | ||
|
|
6eacdd440e | ||
|
|
6e8051b9e6 | ||
|
|
840cf532a9 | ||
|
|
f4bd44996c | ||
|
|
dd8e717ed6 | ||
|
|
6d3a7eeef5 | ||
|
|
83e05e607e | ||
|
|
6b88fe577c | ||
|
|
ea88b377d5 | ||
|
|
bff5e0e738 | ||
|
|
2c39545d24 | ||
|
|
14c38d18fc | ||
|
|
50848aaca2 | ||
|
|
a0fc6022d7 | ||
|
|
f042540311 | ||
|
|
7512e3b7e1 | ||
|
|
6014ebd2e7 | ||
|
|
043565d28a | ||
|
|
2bd853e96f | ||
|
|
d24055cb40 | ||
|
|
9a56f8dca0 | ||
|
|
f5a365c2c8 | ||
|
|
966c67a832 | ||
|
|
c36f03cd2e | ||
|
|
a26b631aae | ||
|
|
f1b5f66374 | ||
|
|
0316676fdc | ||
|
|
70bfd76ced | ||
|
|
59ed51a309 | ||
|
|
628c876b2d | ||
|
|
48ae18a2cb | ||
|
|
ae34a962d7 | ||
|
|
bcf0230772 | ||
|
|
37685a7f42 | ||
|
|
87ab767175 | ||
|
|
9afd63948e | ||
|
|
ceb429b253 | ||
|
|
619f2bf5a9 | ||
|
|
dc667ab9fb | ||
|
|
bf1a1fa5fd | ||
|
|
0dbc154380 | ||
|
|
83e64e2071 | ||
|
|
e1567b6148 | ||
|
|
60a42e9419 | ||
|
|
8529a0c1d3 | ||
|
|
990643c1e0 | ||
|
|
978e26c544 | ||
|
|
0920cb33ee | ||
|
|
c787efd558 | ||
|
|
05c8911363 | ||
|
|
10f835c501 | ||
|
|
d05b7888ab | ||
|
|
0224be8194 | ||
|
|
3d09b66c6b | ||
|
|
f0baffb01f | ||
|
|
8e57e8075d | ||
|
|
017fa5c0b8 | ||
|
|
0cec5bd6f0 | ||
|
|
5425c754a0 | ||
|
|
0a0754e44a | ||
|
|
092e9a50ae | ||
|
|
cddaa0dddc | ||
|
|
21407882fe | ||
|
|
887c5e1142 | ||
|
|
848a73ed40 | ||
|
|
4a50859936 | ||
|
|
d33df92add | ||
|
|
def3fa3929 | ||
|
|
12a979ea75 | ||
|
|
40e69ce8e3 | ||
|
|
7daceb083b | ||
|
|
ff3150d58a | ||
|
|
0024484f5b | ||
|
|
524a236072 | ||
|
|
40c5bdad65 | ||
|
|
bb27ea32cd | ||
|
|
c38e0c7ba8 | ||
|
|
bce513fac6 | ||
|
|
e904628830 | ||
|
|
d7b8b123e6 | ||
|
|
94b6178906 | ||
|
|
b474a83463 | ||
|
|
a179808bfe | ||
|
|
3aa6206e4f | ||
|
|
04225fec72 | ||
|
|
52948f610c | ||
|
|
6ab7fc25d5 | ||
|
|
f9b28cc2b0 | ||
|
|
b00c31cf9e | ||
|
|
8cc3a6d8aa | ||
|
|
b92fe01755 | ||
|
|
92c70766fd | ||
|
|
0b45ecc8a4 | ||
|
|
46c64efd9d | ||
|
|
6c524f7a55 | ||
|
|
5becd50974 | ||
|
|
cb0bcfd67f | ||
|
|
df7bcf78c3 | ||
|
|
50c217964b | ||
|
|
46f7117753 | ||
|
|
5698781c78 | ||
|
|
391d05e2f3 | ||
|
|
318296d8c5 | ||
|
|
6a661f8453 | ||
|
|
371b4fc1fd | ||
|
|
1f59f4cba8 | ||
|
|
4399c2dbc6 | ||
|
|
5009f57279 | ||
|
|
97ad4f89ec | ||
|
|
3ca9ea7b98 | ||
|
|
4bca29ee2c | ||
|
|
5840635baa | ||
|
|
3b3cf8c1f1 | ||
|
|
0827de1864 | ||
|
|
42102a1bfd | ||
|
|
84b38f7b89 | ||
|
|
71925c47ea | ||
|
|
e703982de4 | ||
|
|
5c9e844104 | ||
|
|
8ebb7ae2f5 | ||
|
|
7ac2dbeee0 | ||
|
|
0fe7a0baa6 | ||
|
|
bc96b0bcf6 | ||
|
|
0239b27f4f | ||
|
|
f9532fb59a | ||
|
|
529b1a60e9 | ||
|
|
eb469c756d | ||
|
|
cd38fa3507 | ||
|
|
3d4f6db2a0 | ||
|
|
028efcbd87 | ||
|
|
68be295b75 | ||
|
|
495b5991cf | ||
|
|
653d15504e | ||
|
|
3610087fab | ||
|
|
404f2dccd5 | ||
|
|
6d5de9feaf | ||
|
|
102fbb21ce | ||
|
|
a95f488e71 | ||
|
|
b1de3338ef | ||
|
|
fb608ee1b4 | ||
|
|
3fe38dff05 | ||
|
|
51e6d655b8 | ||
|
|
7a62bf7be2 | ||
|
|
2405caa352 | ||
|
|
6740a1795c | ||
|
|
587c6b97e7 | ||
|
|
49f95b9ef3 | ||
|
|
317c9e037a | ||
|
|
9c55cc3954 | ||
|
|
d4b73c9e88 | ||
|
|
3e920b5ba7 | ||
|
|
77d9b4651f | ||
|
|
9ee8c51edc | ||
|
|
f4e658fc6e | ||
|
|
a8bfaab091 | ||
|
|
c1e0836d6b | ||
|
|
a568f3c818 | ||
|
|
7c14405f8b | ||
|
|
db51d3009f | ||
|
|
bdc08d8285 | ||
|
|
0cef48edbf | ||
|
|
78a2d867fe | ||
|
|
e3dc5b7baf | ||
|
|
ebce88c13d | ||
|
|
d6cc8a1419 | ||
|
|
901e831313 | ||
|
|
98bb7e6630 | ||
|
|
706d1e9929 | ||
|
|
3ebd101eb5 | ||
|
|
d8b59300ce | ||
|
|
170335cbb6 | ||
|
|
53643def07 | ||
|
|
c69ae26c1f | ||
|
|
efc1d059fa | ||
|
|
4738d06e71 | ||
|
|
702ebf402c | ||
|
|
44cb80f74a | ||
|
|
50589386af | ||
|
|
fb0ccf3546 | ||
|
|
e93c1c6f27 | ||
|
|
34e97023f4 | ||
|
|
9eccd9c35b | ||
|
|
a84136d070 | ||
|
|
3cdc6c9d81 | ||
|
|
323fd0d188 | ||
|
|
fd78116f5c | ||
|
|
a54391e96f | ||
|
|
cc9d950601 | ||
|
|
96bf685380 | ||
|
|
1b8665571e | ||
|
|
af467017c2 | ||
|
|
bec1728133 | ||
|
|
81129cefa5 | ||
|
|
7f189260e6 | ||
|
|
3929fe4a63 | ||
|
|
777ffdd692 | ||
|
|
e1ce4c0bf3 | ||
|
|
18f1cdf470 | ||
|
|
f7940112a2 | ||
|
|
392bc33466 | ||
|
|
89a50ed64d | ||
|
|
07554e9525 | ||
|
|
2562c7f796 | ||
|
|
feb0d34213 | ||
|
|
f9473e756d | ||
|
|
b952c99898 | ||
|
|
7189f340a9 | ||
|
|
e3ae0664bb | ||
|
|
ecd7363676 | ||
|
|
9b3df4b90e | ||
|
|
26a3e6f9ff | ||
|
|
c8ce94b40e | ||
|
|
0276e25f71 | ||
|
|
f5af0a9ff4 | ||
|
|
1722448c3b | ||
|
|
c5a39ae77e | ||
|
|
3aa2ce89e8 | ||
|
|
a138c85e64 | ||
|
|
7602abc3cf | ||
|
|
e11163d010 | ||
|
|
d65016042b | ||
|
|
fd0177ae3a | ||
|
|
ba132f0200 | ||
|
|
764a0db68e | ||
|
|
934afb85a4 | ||
|
|
9021fa15dd | ||
|
|
b97bb1809f | ||
|
|
e8b46c1b16 | ||
|
|
a522507020 | ||
|
|
2188c0fd2c | ||
|
|
db44ad3022 | ||
|
|
3b98040623 | ||
|
|
97419b34de | ||
|
|
98b9b70aa4 | ||
|
|
525191f34b | ||
|
|
354b900f15 | ||
|
|
28a5a28b39 | ||
|
|
4421cb9424 | ||
|
|
678ef6b9fd | ||
|
|
da55e97df8 | ||
|
|
a8225a250b | ||
|
|
9516ed41aa | ||
|
|
f5116bc88d | ||
|
|
a85df8027b | ||
|
|
d525928665 | ||
|
|
2ed0eaa121 | ||
|
|
65755af7e3 | ||
|
|
07f441584b | ||
|
|
e0e536b54e | ||
|
|
e58bcb51fc | ||
|
|
7858ad066f | ||
|
|
8e9202acb4 | ||
|
|
f1d2e6c289 | ||
|
|
1112c9f5ce | ||
|
|
84372e0edc | ||
|
|
04de6a2e57 | ||
|
|
d3bdf1403d | ||
|
|
b22b313e4b | ||
|
|
54331dca6f | ||
|
|
3f8be7400d | ||
|
|
7a305e5d9a | ||
|
|
c05f58b776 | ||
|
|
12b482345b | ||
|
|
83aa65c429 | ||
|
|
657e76ba77 | ||
|
|
428dc6e46e | ||
|
|
b944395d66 | ||
|
|
bba6cf4296 | ||
|
|
a33773182e | ||
|
|
445a2ef498 | ||
|
|
e3dc3f7934 | ||
|
|
cccbc9d280 | ||
|
|
8be0a10e91 | ||
|
|
e97140e4d0 | ||
|
|
d3b4f737b9 | ||
|
|
16f68254a8 | ||
|
|
83675fe768 | ||
|
|
d7ac975454 | ||
|
|
0248dd4a0b | ||
|
|
1617692f12 | ||
|
|
b7331b653a | ||
|
|
ba496b8369 | ||
|
|
44687a3cda | ||
|
|
42b0a263a6 | ||
|
|
de177e8850 | ||
|
|
3512e441f4 | ||
|
|
f637ac8603 | ||
|
|
bd787a9fcd | ||
|
|
a781bfb063 | ||
|
|
c0d3b8e7d0 | ||
|
|
379db4cb88 | ||
|
|
097f2c8917 | ||
|
|
b5088ceca6 | ||
|
|
d4ffa5befb | ||
|
|
a737d5fe2f | ||
|
|
d91a4c47f3 | ||
|
|
3efc436d71 | ||
|
|
270b539e8f | ||
|
|
f6547ac54e | ||
|
|
c45b84bd39 | ||
|
|
36e9514e03 | ||
|
|
fbf964864d | ||
|
|
4eb91cdd8e | ||
|
|
a064f248d7 | ||
|
|
b300116adc | ||
|
|
b2a35414aa | ||
|
|
cb40e9682e | ||
|
|
141e18319a | ||
|
|
58ae1140c3 | ||
|
|
087541cb2d | ||
|
|
d300bcfb80 | ||
|
|
0b87867729 | ||
|
|
9b18686168 | ||
|
|
2b30fbcfd5 | ||
|
|
c6a1bc144a | ||
|
|
28cafc2273 | ||
|
|
26a05b547e | ||
|
|
c8cf74dc71 | ||
|
|
663a766a0c | ||
|
|
63ae399db0 | ||
|
|
ae53a8eb65 | ||
|
|
d0c9e589ca | ||
|
|
2ac26e221c | ||
|
|
8c844c794d | ||
|
|
69a4dbda68 | ||
|
|
247d892e69 | ||
|
|
6872289f1a | ||
|
|
0acf2e99c4 | ||
|
|
d021bbfabd | ||
|
|
e9ed248a15 | ||
|
|
e0e3fabfdb | ||
|
|
2fc00f73b6 | ||
|
|
b3c0d84dd3 | ||
|
|
e622f793c3 | ||
|
|
ef7b126ee3 | ||
|
|
cef9c4af03 | ||
|
|
530dbe69ce | ||
|
|
ab94dd69f8 | ||
|
|
2ef29b7f95 | ||
|
|
09ffe421a9 | ||
|
|
36b8ad63d2 | ||
|
|
c64c36b120 | ||
|
|
4cd8b364dd | ||
|
|
3bab1514c3 | ||
|
|
25a1988859 | ||
|
|
726f2ad0a3 | ||
|
|
35d3ed40cd | ||
|
|
a96bb23caf | ||
|
|
54a481c04b | ||
|
|
66c51a3d56 | ||
|
|
2d03991195 | ||
|
|
46c18d9370 | ||
|
|
f8e21cb78b | ||
|
|
0f08ba77c8 | ||
|
|
12f62d2191 | ||
|
|
f963bf6f9a | ||
|
|
40e21bb8ba | ||
|
|
b829bc2dba | ||
|
|
509e4e20e8 | ||
|
|
0fe959df60 | ||
|
|
abc1c5d9ba | ||
|
|
d6f5504311 | ||
|
|
ab15d7d22d | ||
|
|
6cfdabb69a | ||
|
|
5d1cac64c1 | ||
|
|
a4444f1996 | ||
|
|
5bd2077872 | ||
|
|
a02859ad6b | ||
|
|
6cd4e2af48 | ||
|
|
621146bd80 | ||
|
|
0065a71e3d | ||
|
|
fe436f1eb0 | ||
|
|
81c0413c38 | ||
|
|
835f9c65e9 | ||
|
|
d4c14fb6fd | ||
|
|
edcbf8fb4b | ||
|
|
8067f013d2 | ||
|
|
3e1020945e | ||
|
|
68294256a1 | ||
|
|
af429b951d | ||
|
|
2d4de2ef35 | ||
|
|
1dca1f3c19 | ||
|
|
ba5e6eb42d | ||
|
|
584fd585ec | ||
|
|
8037b45cb1 | ||
|
|
0ed646ed09 | ||
|
|
b77194644e | ||
|
|
19da6479a8 | ||
|
|
65dc06c989 | ||
|
|
4e3ec8ef24 | ||
|
|
c4e4f2d9fa | ||
|
|
f0fc9f8846 | ||
|
|
b517f9cc7f | ||
|
|
488b1999f3 | ||
|
|
d3c5cd6ee2 | ||
|
|
1d2cde763c | ||
|
|
229a4e03e3 | ||
|
|
a1f2d69552 | ||
|
|
14afbc7ad6 | ||
|
|
9f2f405c3f | ||
|
|
c2108d6d43 | ||
|
|
7266936138 | ||
|
|
7f65d2745d | ||
|
|
147b047487 | ||
|
|
3922263f8c | ||
|
|
2c28197c2a | ||
|
|
b9c2c477cc | ||
|
|
45c721a2ca | ||
|
|
788aec156f | ||
|
|
c082292bb8 | ||
|
|
0107df0a2b | ||
|
|
b8fff8c508 | ||
|
|
c753eeb6ec | ||
|
|
6485dd45a3 | ||
|
|
a19a59652f | ||
|
|
7f13fa5d5f | ||
|
|
6d5d386c9f | ||
|
|
52e1dd6099 | ||
|
|
60bf370a7d | ||
|
|
24d9dc7469 | ||
|
|
9b4e7691f4 | ||
|
|
de2b7b6dcc | ||
|
|
8fc66027f7 | ||
|
|
52ada7c614 | ||
|
|
4f2069ee9a | ||
|
|
559d8a78d1 | ||
|
|
30649e7fee | ||
|
|
12a7075fda | ||
|
|
8dede082d5 | ||
|
|
5deeb82e0e | ||
|
|
a1b462c94a | ||
|
|
baadc2dc93 | ||
|
|
d5eda977ce | ||
|
|
bd1658baae | ||
|
|
5827a93c2f | ||
|
|
008717fcc9 | ||
|
|
fb590d41ff | ||
|
|
e431b2b898 | ||
|
|
3bada745ea | ||
|
|
060b719d44 | ||
|
|
e8cb09f70f | ||
|
|
d6ec6cf719 | ||
|
|
4a440f67c8 | ||
|
|
74bbc72d28 | ||
|
|
8aec369df7 | ||
|
|
478efe55c6 | ||
|
|
cf57e35e37 | ||
|
|
54c49abbcb | ||
|
|
b3750cac62 | ||
|
|
14d3af25eb | ||
|
|
19bffaa2fd | ||
|
|
f1502119a2 | ||
|
|
b193d9e67b | ||
|
|
a38f8d059e | ||
|
|
fe1f56c08f | ||
|
|
f3de91c2bb | ||
|
|
6b73138c73 | ||
|
|
f2dffc5237 | ||
|
|
616249e680 | ||
|
|
f9fbe18abf | ||
|
|
87dccbb8dc | ||
|
|
3d31858ee3 | ||
|
|
d7b189ce56 | ||
|
|
c024147ede | ||
|
|
d827e9aa72 | ||
|
|
0cd8cbc83d | ||
|
|
0047fa35dd | ||
|
|
2dac92cc8c | ||
|
|
833bbf7186 | ||
|
|
34e0ff3497 | ||
|
|
e6caddba19 | ||
|
|
5e7c6eb31e | ||
|
|
c67ba85311 | ||
|
|
588b6ed3fc | ||
|
|
f75fc6b2a8 | ||
|
|
7727f8777a | ||
|
|
d258650ad4 | ||
|
|
74183d91cb | ||
|
|
dbd69af90d | ||
|
|
da369be096 | ||
|
|
c7cbb1e6ad | ||
|
|
4f39946ea3 | ||
|
|
ffed7beab6 | ||
|
|
cb5ed6276d | ||
|
|
bb50e00447 | ||
|
|
d57207510d | ||
|
|
ebd83171df | ||
|
|
42cc56c0f6 | ||
|
|
b2c0fab3b9 | ||
|
|
b1a9209f9f | ||
|
|
7432acc215 | ||
|
|
75aa6a0840 | ||
|
|
0ed7930976 | ||
|
|
1e4de986a8 | ||
|
|
8670d0b3bc | ||
|
|
38a4a0aa3b | ||
|
|
2d54ea112a | ||
|
|
a062cbd439 | ||
|
|
eacf4c886e | ||
|
|
29d15de38e | ||
|
|
cf604f6b93 | ||
|
|
c598785b6f | ||
|
|
bb0d9573a9 | ||
|
|
92ce2dcbc3 | ||
|
|
8cfe8e590d | ||
|
|
6c6294571a | ||
|
|
a82a79bf3f | ||
|
|
e6c9b04386 | ||
|
|
f15cfbb208 | ||
|
|
bf85379619 | ||
|
|
89ab609732 | ||
|
|
b5baf5c807 | ||
|
|
0bd6dfc1a2 | ||
|
|
c4f2c6d24d | ||
|
|
a9ed1de5f7 | ||
|
|
7486dee082 | ||
|
|
029f9cca97 | ||
|
|
610716d3d1 | ||
|
|
6e37f197b0 | ||
|
|
53109a0127 | ||
|
|
099213f365 | ||
|
|
b368fbd1a0 | ||
|
|
120d60223a | ||
|
|
84195868fc | ||
|
|
e5a5ae8f24 | ||
|
|
184e1908c8 | ||
|
|
a3e3b3d8aa | ||
|
|
59acc7e48a | ||
|
|
6dbd53b387 | ||
|
|
8d28c9ab40 | ||
|
|
55cdfc6fd2 | ||
|
|
e836e55489 | ||
|
|
13242f55c5 | ||
|
|
1494faee1f | ||
|
|
64d57ba466 | ||
|
|
10b0c113a4 | ||
|
|
14f984fbcf | ||
|
|
c972205214 | ||
|
|
1975c056bc | ||
|
|
b4ab84df58 | ||
|
|
64876697b5 | ||
|
|
392a1fe16d | ||
|
|
aa8023407e | ||
|
|
3fa9d3d758 | ||
|
|
62f668fc3f | ||
|
|
3112e08088 | ||
|
|
4aa249c8a5 | ||
|
|
e05ac1e91f | ||
|
|
b2207e5b04 | ||
|
|
8fb7365a61 | ||
|
|
62b8d0ed82 | ||
|
|
711248f678 | ||
|
|
a3c9721d8f | ||
|
|
763dbc0ac6 | ||
|
|
7f9f66e542 | ||
|
|
0c339e0647 | ||
|
|
29f3e67655 | ||
|
|
2779bde2a2 | ||
|
|
24dfeed89c | ||
|
|
a0c0870b87 | ||
|
|
47e758d8f1 | ||
|
|
9d0ab3b97b | ||
|
|
70ab1c3b6f | ||
|
|
eee891c8cb | ||
|
|
653cdd286b | ||
|
|
9e4754fb4a | ||
|
|
01f1a0090e | ||
|
|
a0658c06bf | ||
|
|
6ba50f3aa7 | ||
|
|
2fc676709d | ||
|
|
ee1fcfbeca | ||
|
|
3584b99caa | ||
|
|
11568caf97 | ||
|
|
32a286d530 | ||
|
|
fbd529204b | ||
|
|
bc198b89ca | ||
|
|
202f0bbbc9 | ||
|
|
949ea163cd | ||
|
|
003e4c21e0 | ||
|
|
3de3c6a5fc | ||
|
|
a85b9f798d | ||
|
|
58a190fd69 | ||
|
|
7162a3defe | ||
|
|
36653586a0 | ||
|
|
1b3d14237a | ||
|
|
365f896c36 | ||
|
|
f13dcac9ef | ||
|
|
91530f1005 | ||
|
|
3e2b9dc91a | ||
|
|
13eed4ac5b | ||
|
|
801a31302d | ||
|
|
4888644990 | ||
|
|
8f32c5b929 | ||
|
|
273326b89b | ||
|
|
8e9f927e3d | ||
|
|
3a0406847b | ||
|
|
918821b7e0 | ||
|
|
b149c7a344 | ||
|
|
f511afe40e | ||
|
|
7c804cabe6 | ||
|
|
fe70c72691 | ||
|
|
79b098c409 | ||
|
|
b63b37ac36 | ||
|
|
2e0ee5f5b2 | ||
|
|
eb8b48aeb4 | ||
|
|
f1de792877 | ||
|
|
a438dbfcf6 | ||
|
|
bf33f4c91c | ||
|
|
9a5461db24 | ||
|
|
4063730925 | ||
|
|
4f5cb99ff5 | ||
|
|
e384c182bc | ||
|
|
68510596d3 | ||
|
|
cee834bb5e | ||
|
|
c0f750d9b3 | ||
|
|
1b435f1f1f | ||
|
|
0e851bdf75 | ||
|
|
7da72563dd | ||
|
|
2f1d4d10e0 | ||
|
|
c9ad2e1451 | ||
|
|
6fe1702ee1 | ||
|
|
e2fe94cbe7 | ||
|
|
6ec47ea736 | ||
|
|
7d46852aa9 | ||
|
|
1392471854 | ||
|
|
a336a6b0b5 | ||
|
|
ecaf70128a | ||
|
|
e1e6a32c51 | ||
|
|
fd53cdea17 | ||
|
|
0e5eb19929 | ||
|
|
8ffcd386ae | ||
|
|
6ee9e13b26 | ||
|
|
2f25085d60 | ||
|
|
e69e3e04a1 | ||
|
|
5fb721f7a7 | ||
|
|
79359ed4e2 | ||
|
|
e120828042 | ||
|
|
d71699d7cd | ||
|
|
0781dac78f | ||
|
|
42562a12ed | ||
|
|
254c42864b | ||
|
|
ab63f9393f | ||
|
|
9e0b4a9d4d | ||
|
|
f32bc648f0 | ||
|
|
6f2e869213 | ||
|
|
9641434163 | ||
|
|
b3c0eab7dc | ||
|
|
9b8e3d082d | ||
|
|
46f5cc9609 | ||
|
|
b44461da33 | ||
|
|
d89486b0f0 | ||
|
|
2e47f2dc72 | ||
|
|
8db02bd550 | ||
|
|
182658c88c | ||
|
|
4c154c3019 | ||
|
|
450d617dec | ||
|
|
98d8a3d60f | ||
|
|
f455312944 | ||
|
|
e65ea5ede0 | ||
|
|
c2375f2fa8 | ||
|
|
3d50b3736a | ||
|
|
0c481ea51d | ||
|
|
df01824d10 | ||
|
|
928938a6a1 | ||
|
|
5ed3f5649b | ||
|
|
3b728fdb76 | ||
|
|
e2ed73209a | ||
|
|
0670e0b287 | ||
|
|
e492a5578c | ||
|
|
ec6ba99715 | ||
|
|
26adf1d560 | ||
|
|
3b3b33ea29 | ||
|
|
3ff133c166 | ||
|
|
bcff73c9cc | ||
|
|
54fdd6ae5f | ||
|
|
13d528c569 | ||
|
|
2960549682 | ||
|
|
522e82b7b7 | ||
|
|
171fd18ba5 | ||
|
|
80a59db094 | ||
|
|
282527ef16 | ||
|
|
0f528544b5 | ||
|
|
30a36a8a00 | ||
|
|
9fc5a9316e | ||
|
|
34e9d11786 | ||
|
|
605b39f617 | ||
|
|
528c7afd18 | ||
|
|
23d5b78fdb | ||
|
|
bb6f1efe63 | ||
|
|
30d3bb2990 | ||
|
|
1e0db9c2c8 | ||
|
|
74c582fb04 | ||
|
|
03617b2f98 | ||
|
|
58a16bcf57 | ||
|
|
7a49309035 | ||
|
|
1c10340943 | ||
|
|
01be1713cf | ||
|
|
0a8ba31b9b | ||
|
|
67c1beb322 | ||
|
|
4a3b10f0ed | ||
|
|
b2b5676698 | ||
|
|
db6124482d | ||
|
|
ccf84fb698 | ||
|
|
081f3028ee | ||
|
|
5e21e0bec6 | ||
|
|
1b93e26b58 | ||
|
|
92f433c3cf | ||
|
|
53052fe019 | ||
|
|
2d2ea15246 | ||
|
|
d35b00352f | ||
|
|
566f1afcd4 | ||
|
|
05f486ef49 | ||
|
|
18fa004254 | ||
|
|
5258dce73b | ||
|
|
1c641b1c28 | ||
|
|
dbdb8f7606 | ||
|
|
5f64b1a255 | ||
|
|
524bdaeb33 | ||
|
|
cc45b5e57b | ||
|
|
659086f22f | ||
|
|
fa8d022813 | ||
|
|
78f406af8a | ||
|
|
8d2af32e4d | ||
|
|
60f30cd827 | ||
|
|
977bbd7643 | ||
|
|
44f5753bd8 | ||
|
|
04471f7d97 | ||
|
|
1295364986 | ||
|
|
aee7843bec | ||
|
|
11c55fd4c2 | ||
|
|
a64fa6d478 | ||
|
|
bb29630d57 | ||
|
|
f0ee93c5a7 | ||
|
|
eee3c759b6 | ||
|
|
9e4dc7f349 | ||
|
|
0cc199d351 | ||
|
|
6c2a14839e | ||
|
|
d56fdca618 | ||
|
|
f74e3031bd | ||
|
|
95b8d71bd9 | ||
|
|
627c9eb0bf | ||
|
|
d9800ad95a | ||
|
|
93152218a7 | ||
|
|
32d32e3743 | ||
|
|
dde988bd38 | ||
|
|
0bb8a8fabe | ||
|
|
57cc814b8b | ||
|
|
b66d28964b | ||
|
|
2f51b9da1c | ||
|
|
31c4c37156 | ||
|
|
755b8000f6 | ||
|
|
5a62501307 | ||
|
|
df40e862f4 | ||
|
|
489d9f9926 | ||
|
|
251721b890 | ||
|
|
5706b9149c | ||
|
|
5779f93ec6 | ||
|
|
34c3a1580a | ||
|
|
49cf490697 | ||
|
|
2a9893d0f0 | ||
|
|
67c2dcd90d | ||
|
|
f33716cc17 | ||
|
|
5e52a657df | ||
|
|
5d767bbc49 | ||
|
|
9000e9dd41 | ||
|
|
aefbe71765 | ||
|
|
b2faf339ce | ||
|
|
76a184eb07 | ||
|
|
4eea90c26c | ||
|
|
327ed924a3 | ||
|
|
fa536a8693 | ||
|
|
ab19b94811 | ||
|
|
30c1e8289f | ||
|
|
764c765d29 | ||
|
|
c7d938c2c4 | ||
|
|
2cf127f2d3 | ||
|
|
75043079d0 | ||
|
|
8f109890c2 | ||
|
|
ef583d2fde | ||
|
|
300eb6b902 | ||
|
|
ae97bb5068 | ||
|
|
97e3371046 | ||
|
|
3fadbefab9 | ||
|
|
db46dc79bb | ||
|
|
23762d39ba | ||
|
|
f5e7337db6 | ||
|
|
35caf115f8 | ||
|
|
cd57ba90f5 | ||
|
|
d8b54ddf4a | ||
|
|
2500767f1b | ||
|
|
968b2fdaf1 | ||
|
|
3a72fb39cb | ||
|
|
0c0427bfbd | ||
|
|
1074b39988 | ||
|
|
8a3c740f9e | ||
|
|
32ce682238 | ||
|
|
c699e255a1 | ||
|
|
bf68512e7d | ||
|
|
8c93896c48 | ||
|
|
40e0b2c57f | ||
|
|
4941ed58d5 | ||
|
|
517c3145f4 | ||
|
|
b90412742e | ||
|
|
83f0802578 | ||
|
|
01a628d96d | ||
|
|
74fd04c67e | ||
|
|
6af0e33eed | ||
|
|
0ecd50b80d | ||
|
|
4ff23b4eab | ||
|
|
6850c27dd6 | ||
|
|
2cff185c00 | ||
|
|
f6b0c135ce | ||
|
|
fe10811e29 | ||
|
|
9fada617b9 | ||
|
|
cb36410c91 | ||
|
|
2bdbd9e7a0 | ||
|
|
dc612f0219 | ||
|
|
023b337ff0 | ||
|
|
0760bec3ff | ||
|
|
0fd4a2ea38 | ||
|
|
addc7045ba | ||
|
|
6d4487e4b4 | ||
|
|
8dd05c9fce | ||
|
|
0bc40bc4ea | ||
|
|
d09930469c | ||
|
|
54359fff39 | ||
|
|
7d5f98b6a0 | ||
|
|
8ff880faa6 | ||
|
|
6970827a84 | ||
|
|
1bd745eccf | ||
|
|
f1b6a7842a | ||
|
|
7728162d7a | ||
|
|
bfcee8ec9f | ||
|
|
1509dc497e | ||
|
|
707407dd49 | ||
|
|
4bd6529c05 | ||
|
|
f8f81db36d | ||
|
|
b3582dfd31 | ||
|
|
7c3404ef1f | ||
|
|
c8620f35e1 | ||
|
|
38766816ac | ||
|
|
2048fed110 | ||
|
|
2633d38a63 | ||
|
|
183c8cbb3a | ||
|
|
95dbf1190a | ||
|
|
bb108bf00e | ||
|
|
f612aeb22b | ||
|
|
ab063977ad | ||
|
|
36609ee305 | ||
|
|
2d0927d0b3 | ||
|
|
795506a486 | ||
|
|
cfc4910068 | ||
|
|
cb164ef629 | ||
|
|
e0155fbd66 | ||
|
|
de63b6a850 | ||
|
|
f735f47448 | ||
|
|
a18646deb2 | ||
|
|
247d45af05 | ||
|
|
96c35185f0 | ||
|
|
d3474dfff3 | ||
|
|
c1580be7d3 | ||
|
|
0245a67831 | ||
|
|
8537e7c94e | ||
|
|
47fb42cf1f | ||
|
|
b0c7ae4d29 | ||
|
|
7f900395ec | ||
|
|
7c376cb4d6 | ||
|
|
f84b19748d | ||
|
|
562276098c | ||
|
|
38da98d2d6 | ||
|
|
c377830898 | ||
|
|
66a78ce819 | ||
|
|
fa2e154b41 | ||
|
|
dd46fde384 | ||
|
|
7c69730ad2 | ||
|
|
9297befe5c | ||
|
|
cbbfcd20b4 | ||
|
|
07f66417dd | ||
|
|
e6d63d70be | ||
|
|
f48c0abcbe | ||
|
|
cab8036db3 | ||
|
|
da80cc6479 | ||
|
|
7f1e9c1907 | ||
|
|
120cae9d41 | ||
|
|
376d283ba3 | ||
|
|
d4a9ff4d1f | ||
|
|
332b98bd86 | ||
|
|
aaa249bda9 | ||
|
|
c75d8939f8 | ||
|
|
df919e6ab5 | ||
|
|
e82c1e7259 | ||
|
|
69b2cb5ea6 | ||
|
|
07797f3fff | ||
|
|
a21a4f46c7 | ||
|
|
b43b6a61ab | ||
|
|
b7ce5db782 | ||
|
|
1874cea63e | ||
|
|
84dcb91119 | ||
|
|
e9ff1be96c | ||
|
|
8520455042 | ||
|
|
01d3a72a0e | ||
|
|
1bf9ff7493 | ||
|
|
1bd9bfefeb | ||
|
|
9a52eefc99 | ||
|
|
83fbbaea9a | ||
|
|
3def940574 | ||
|
|
8be0f857e8 | ||
|
|
7769915a0b | ||
|
|
73250ff4e3 | ||
|
|
95afd642f5 | ||
|
|
02d95c033c | ||
|
|
c148845a98 | ||
|
|
927b2ab3f8 | ||
|
|
01653b8077 | ||
|
|
24d4bc82d7 | ||
|
|
b12e4e82aa | ||
|
|
0cf2638b3b | ||
|
|
f4b3add8ab | ||
|
|
702f1401d4 | ||
|
|
9bdce088c8 | ||
|
|
fdf476a5a1 | ||
|
|
9d1342aeb6 | ||
|
|
e0c7db1021 | ||
|
|
90128c5a9d | ||
|
|
016eda9f3c | ||
|
|
a75fe69984 | ||
|
|
3d7b374bef | ||
|
|
64f0178b75 | ||
|
|
fff3ad518c | ||
|
|
071cc422c7 | ||
|
|
e08d373be3 | ||
|
|
4f4767c9e0 | ||
|
|
0b6a39768a | ||
|
|
625750eeef | ||
|
|
099f521b7e | ||
|
|
e868add5a3 | ||
|
|
28664d6bf1 | ||
|
|
da44d4267e | ||
|
|
77947f212e | ||
|
|
bb78de09d1 | ||
|
|
bd212393ae | ||
|
|
471fe7d58f | ||
|
|
fb15fa65f2 | ||
|
|
e10b81858a | ||
|
|
7a998a091e | ||
|
|
ba7760b705 | ||
|
|
a889f0bfd5 | ||
|
|
217d31ec1c | ||
|
|
f21316ebe8 | ||
|
|
1c7ac67fba | ||
|
|
59c282b184 | ||
|
|
fd9d9d276b | ||
|
|
b3bfecde39 | ||
|
|
168ede2be0 | ||
|
|
c2980217a8 | ||
|
|
d74ee6d743 | ||
|
|
8df6b22f4f | ||
|
|
f72a82359c | ||
|
|
c65dd3ea3a | ||
|
|
75651dc8b0 | ||
|
|
72f98d26e6 | ||
|
|
00e048995c | ||
|
|
5977b1125b | ||
|
|
5590fc7a53 | ||
|
|
e9e665d042 | ||
|
|
c97dfb2281 | ||
|
|
64a65351b9 | ||
|
|
ee80f6218a | ||
|
|
fee8ddd54b | ||
|
|
9c6edab726 | ||
|
|
09e64e8ff6 | ||
|
|
af2c6de9ae | ||
|
|
b8300268bf | ||
|
|
81a94b8048 | ||
|
|
efeef97f5e | ||
|
|
82969e4ba3 | ||
|
|
64467f6ab9 | ||
|
|
e60f541559 | ||
|
|
809b7482df | ||
|
|
2d4ea61c93 | ||
|
|
eab24f3e48 | ||
|
|
b33f2c40d8 | ||
|
|
6302dec938 | ||
|
|
681d78b6cf | ||
|
|
5169f455c9 | ||
|
|
a009dd6caf | ||
|
|
668e6415c0 | ||
|
|
71a740d65d | ||
|
|
03ac3b0840 | ||
|
|
bbc3130af2 | ||
|
|
2fbe359846 | ||
|
|
9f0cfed600 | ||
|
|
ec72cb7260 | ||
|
|
cd61269389 | ||
|
|
da00fa98f9 | ||
|
|
dc28922ccb | ||
|
|
06d01c8c9d | ||
|
|
c78b6967cd | ||
|
|
264161c182 | ||
|
|
c7b369a7be | ||
|
|
7023caba94 | ||
|
|
24a2c3d8db | ||
|
|
a18b683d12 | ||
|
|
888787fb2d | ||
|
|
71c238d4ec | ||
|
|
7d7c9c0fde | ||
|
|
f5af95cc9d | ||
|
|
f673f8bb55 | ||
|
|
185a288645 | ||
|
|
ab1df03418 | ||
|
|
a170e60daa | ||
|
|
e748da2abe | ||
|
|
ec2e15f086 | ||
|
|
d71a75fea2 | ||
|
|
3cb35e8679 | ||
|
|
3e76af4912 | ||
|
|
545bc0d21c | ||
|
|
7a763a9851 | ||
|
|
985f09ff88 | ||
|
|
960bcc9614 | ||
|
|
b920f3cc6b | ||
|
|
cac1a3f34e | ||
|
|
917586a0e0 | ||
|
|
e74253d2de | ||
|
|
518a72d7e7 | ||
|
|
f9cfaef5aa | ||
|
|
aa4bc9d241 | ||
|
|
35dd1907b6 | ||
|
|
a4c1a701bc | ||
|
|
6088b1f995 | ||
|
|
136834038d | ||
|
|
9cde231665 | ||
|
|
889124b5ca | ||
|
|
db04d612e0 | ||
|
|
2fa0fabb05 | ||
|
|
f3b4565e8c | ||
|
|
728349bc4b | ||
|
|
7e2ad215c2 | ||
|
|
1a3d296d87 | ||
|
|
e2ee95d9b2 | ||
|
|
8a7142d763 | ||
|
|
f2be2ead35 | ||
|
|
7b491c7110 | ||
|
|
35a78c06d0 | ||
|
|
29460606b2 | ||
|
|
b268b37482 | ||
|
|
1f3c6ce035 | ||
|
|
160238220f | ||
|
|
1eed1b51b8 | ||
|
|
950bc2c7fb | ||
|
|
fe5390c068 | ||
|
|
6bc7b91dd1 | ||
|
|
b1389603e0 | ||
|
|
38308dc02d | ||
|
|
ea58596a56 | ||
|
|
570d629a14 | ||
|
|
9898b490df | ||
|
|
90cfe677bc | ||
|
|
4621859936 | ||
|
|
4f8a9580aa | ||
|
|
cc1af1da06 | ||
|
|
f1f299c4b8 | ||
|
|
32d65236bf | ||
|
|
1cf4b54cba | ||
|
|
84c398d6f4 | ||
|
|
e60579d9f3 | ||
|
|
2941a1142b | ||
|
|
8432d9b692 | ||
|
|
a2b536030a | ||
|
|
bea33af310 | ||
|
|
782244a7b8 | ||
|
|
bd65d8947f | ||
|
|
9c6a1d80d6 | ||
|
|
d3b5a41830 | ||
|
|
579b05e424 | ||
|
|
fc84567923 | ||
|
|
46ae88c1f0 | ||
|
|
9e34e6e316 | ||
|
|
d7f5c6f979 | ||
|
|
e4f0b4c1b7 | ||
|
|
bdc58cc33f | ||
|
|
e04505a669 | ||
|
|
4da461d90a | ||
|
|
b31cb22d8b | ||
|
|
4f9d0397b5 | ||
|
|
177164b5a2 | ||
|
|
b59d7197ff | ||
|
|
5f9a667470 | ||
|
|
e4cf5f321b | ||
|
|
2125a4cf47 | ||
|
|
c577ed92e7 | ||
|
|
7448e588ff | ||
|
|
a49a34ef3d | ||
|
|
d2b92e8170 | ||
|
|
160a11a0a7 | ||
|
|
28c97a95cd | ||
|
|
7afb2944b2 | ||
|
|
d46bbc486f | ||
|
|
2af0b0b4f3 | ||
|
|
8c550d0157 | ||
|
|
6d40b2a38b | ||
|
|
8ffd1f69d7 | ||
|
|
79c8b1fceb | ||
|
|
5840f880a9 | ||
|
|
afbc57f2ad | ||
|
|
b096c0b8f2 | ||
|
|
c5c0a0699b | ||
|
|
2b8291d18f | ||
|
|
fe409f1a43 | ||
|
|
1d40cc2104 | ||
|
|
de942894ff | ||
|
|
748c2ad273 | ||
|
|
45a34d6b75 | ||
|
|
91782bb6c8 | ||
|
|
d0c62aae7a | ||
|
|
435d9945a3 | ||
|
|
c292ba892b | ||
|
|
15b1f118b5 | ||
|
|
afeac2f099 | ||
|
|
51659ee606 | ||
|
|
45e3dab00d | ||
|
|
6b6ebe7e30 | ||
|
|
e15d7cde86 | ||
|
|
dc84f21dd4 | ||
|
|
aaa3976a29 | ||
|
|
e339de22d7 | ||
|
|
bf8c8521cd | ||
|
|
76e0f11916 | ||
|
|
6bac405d40 | ||
|
|
1c97d3f5fd | ||
|
|
9125439c3a | ||
|
|
aa50a8cde0 | ||
|
|
231f6013bb | ||
|
|
b6926e8e2e | ||
|
|
ec0ed8aef5 | ||
|
|
ba5e27e4ec | ||
|
|
1f1cd2bc39 | ||
|
|
ead906aed0 | ||
|
|
eecf3472ff | ||
|
|
0c9a53bf1b | ||
|
|
47e4f39436 | ||
|
|
6cde287a1e | ||
|
|
fcdd638134 | ||
|
|
f19bf57afc | ||
|
|
8e8b962266 | ||
|
|
62493d1115 | ||
|
|
9f60561d6f | ||
|
|
ebe5132576 | ||
|
|
14df93ef3e | ||
|
|
3dbb61d9af | ||
|
|
9c37473256 | ||
|
|
9bcfe176d4 | ||
|
|
3181b461aa | ||
|
|
3d573d8736 | ||
|
|
c9e297ddb6 | ||
|
|
2560280d21 | ||
|
|
333ea75e45 | ||
|
|
bb6b1c33ae | ||
|
|
2ad69e12ce | ||
|
|
df5d171cd7 | ||
|
|
fa4c03da65 | ||
|
|
c0b1f2ff25 | ||
|
|
1b496c5fc3 | ||
|
|
08319101f4 | ||
|
|
3e11c2d05d | ||
|
|
18b6f4b519 | ||
|
|
4b0a22a8ba | ||
|
|
a2bd0097c5 | ||
|
|
ce25b652b9 | ||
|
|
21c2c9df33 | ||
|
|
9d48766c02 | ||
|
|
cd116977cc | ||
|
|
710eef317a | ||
|
|
3d777bb386 | ||
|
|
a970d3b69b | ||
|
|
1bd6fce7dc | ||
|
|
f042e302d8 | ||
|
|
72d10903d5 | ||
|
|
a5ccc89748 | ||
|
|
a6e2e1d4bb | ||
|
|
cf691e85c8 | ||
|
|
67bc05eb35 | ||
|
|
174d3bf057 | ||
|
|
a8640c759d | ||
|
|
d68a778e3b | ||
|
|
67d6fcb0f6 | ||
|
|
abee146199 | ||
|
|
ff73f6f741 | ||
|
|
2f0fefeb74 | ||
|
|
3f78487769 | ||
|
|
ab556f4dff | ||
|
|
7e69d117c6 | ||
|
|
a3847d830c |
243 changed files with 16936 additions and 5881 deletions
33
.coveragerc
33
.coveragerc
|
|
@ -1,33 +0,0 @@
|
||||||
[run]
|
|
||||||
branch = True
|
|
||||||
source = .
|
|
||||||
omit =
|
|
||||||
.tox/*
|
|
||||||
/usr/*
|
|
||||||
setup.py
|
|
||||||
# Don't complain if non-runnable code isn't run
|
|
||||||
*/__main__.py
|
|
||||||
pre_commit/color_windows.py
|
|
||||||
|
|
||||||
[report]
|
|
||||||
show_missing = True
|
|
||||||
skip_covered = True
|
|
||||||
exclude_lines =
|
|
||||||
# Have to re-enable the standard pragma
|
|
||||||
\#\s*pragma: no cover
|
|
||||||
# We optionally substitute this
|
|
||||||
${COVERAGE_IGNORE_WINDOWS}
|
|
||||||
|
|
||||||
# Don't complain if tests don't hit defensive assertion code:
|
|
||||||
^\s*raise AssertionError\b
|
|
||||||
^\s*raise NotImplementedError\b
|
|
||||||
^\s*return NotImplemented\b
|
|
||||||
^\s*raise$
|
|
||||||
|
|
||||||
# Don't complain if non-runnable code isn't run:
|
|
||||||
^if __name__ == ['"]__main__['"]:$
|
|
||||||
|
|
||||||
[html]
|
|
||||||
directory = coverage-html
|
|
||||||
|
|
||||||
# vim:ft=dosini
|
|
||||||
54
.github/ISSUE_TEMPLATE/00_bug.yaml
vendored
Normal file
54
.github/ISSUE_TEMPLATE/00_bug.yaml
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
name: bug report
|
||||||
|
description: something went wrong
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
this is for issues for `pre-commit` (the framework).
|
||||||
|
if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues]
|
||||||
|
|
||||||
|
[pre-commit.ci]: https://pre-commit.ci
|
||||||
|
[pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues
|
||||||
|
- type: input
|
||||||
|
id: search
|
||||||
|
attributes:
|
||||||
|
label: search you tried in the issue tracker
|
||||||
|
placeholder: ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
95% of issues created are duplicates.
|
||||||
|
please try extra hard to find them first.
|
||||||
|
it's very unlikely your problem is unique.
|
||||||
|
- type: textarea
|
||||||
|
id: freeform
|
||||||
|
attributes:
|
||||||
|
label: describe your issue
|
||||||
|
placeholder: 'I was doing ... I ran ... I expected ... I got ...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: pre-commit --version
|
||||||
|
placeholder: pre-commit x.x.x
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: configuration
|
||||||
|
attributes:
|
||||||
|
label: .pre-commit-config.yaml
|
||||||
|
description: (auto-rendered as yaml, no need for backticks)
|
||||||
|
placeholder: 'repos: ...'
|
||||||
|
render: yaml
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: error-log
|
||||||
|
attributes:
|
||||||
|
label: '~/.cache/pre-commit/pre-commit.log (if present)'
|
||||||
|
placeholder: "### version information\n..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
38
.github/ISSUE_TEMPLATE/01_feature.yaml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/01_feature.yaml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
name: feature request
|
||||||
|
description: something new
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
this is for issues for `pre-commit` (the framework).
|
||||||
|
if you are reporting an issue for [pre-commit.ci] please report it at [pre-commit-ci/issues]
|
||||||
|
|
||||||
|
[pre-commit.ci]: https://pre-commit.ci
|
||||||
|
[pre-commit-ci/issues]: https://github.com/pre-commit-ci/issues
|
||||||
|
- type: input
|
||||||
|
id: search
|
||||||
|
attributes:
|
||||||
|
label: search you tried in the issue tracker
|
||||||
|
placeholder: ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
95% of issues created are duplicates.
|
||||||
|
please try extra hard to find them first.
|
||||||
|
it's very unlikely your feature idea is a new one.
|
||||||
|
- type: textarea
|
||||||
|
id: freeform
|
||||||
|
attributes:
|
||||||
|
label: describe your actual problem
|
||||||
|
placeholder: 'I want to do ... I tried ... It does not work because ...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: pre-commit --version
|
||||||
|
placeholder: pre-commit x.x.x
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: documentation
|
||||||
|
url: https://pre-commit.com
|
||||||
|
about: please check the docs first
|
||||||
|
- name: pre-commit.ci issues
|
||||||
|
url: https://github.com/pre-commit-ci/issues
|
||||||
|
about: please report issues about pre-commit.ci here
|
||||||
9
.github/actions/pre-test/action.yml
vendored
Normal file
9
.github/actions/pre-test/action.yml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
inputs:
|
||||||
|
env:
|
||||||
|
default: ${{ matrix.env }}
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: asottile/workflows/.github/actions/latest-git@v1.4.0
|
||||||
|
if: inputs.env == 'py39' && runner.os == 'Linux'
|
||||||
84
.github/workflows/languages.yaml
vendored
Normal file
84
.github/workflows/languages.yaml
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
name: languages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, test-me-*]
|
||||||
|
tags: '*'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vars:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
languages: ${{ steps.vars.outputs.languages }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: install deps
|
||||||
|
run: python -mpip install -e . -r requirements-dev.txt
|
||||||
|
- name: vars
|
||||||
|
run: testing/languages ${{ github.event_name == 'push' && '--all' || '' }}
|
||||||
|
id: vars
|
||||||
|
language:
|
||||||
|
needs: [vars]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: needs.vars.outputs.languages != '[]'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include: ${{ fromJSON(needs.vars.outputs.languages) }}
|
||||||
|
steps:
|
||||||
|
- uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
|
||||||
|
- run: echo "$CONDA\Scripts" >> "$GITHUB_PATH"
|
||||||
|
shell: bash
|
||||||
|
if: matrix.os == 'windows-latest' && matrix.language == 'conda'
|
||||||
|
- run: testing/get-coursier.sh
|
||||||
|
shell: bash
|
||||||
|
if: matrix.language == 'coursier'
|
||||||
|
- run: testing/get-dart.sh
|
||||||
|
shell: bash
|
||||||
|
if: matrix.language == 'dart'
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
lua5.3 \
|
||||||
|
liblua5.3-dev \
|
||||||
|
luarocks
|
||||||
|
if: matrix.os == 'ubuntu-latest' && matrix.language == 'lua'
|
||||||
|
- run: |
|
||||||
|
echo 'C:\Strawberry\perl\bin' >> "$GITHUB_PATH"
|
||||||
|
echo 'C:\Strawberry\perl\site\bin' >> "$GITHUB_PATH"
|
||||||
|
echo 'C:\Strawberry\c\bin' >> "$GITHUB_PATH"
|
||||||
|
shell: bash
|
||||||
|
if: matrix.os == 'windows-latest' && matrix.language == 'perl'
|
||||||
|
- uses: haskell/actions/setup@v2
|
||||||
|
if: matrix.language == 'haskell'
|
||||||
|
- uses: r-lib/actions/setup-r@v2
|
||||||
|
if: matrix.os == 'ubuntu-latest' && matrix.language == 'r'
|
||||||
|
|
||||||
|
- name: install deps
|
||||||
|
run: python -mpip install -e . -r requirements-dev.txt
|
||||||
|
- name: run tests
|
||||||
|
run: coverage run -m pytest tests/languages/${{ matrix.language }}_test.py
|
||||||
|
- name: check coverage
|
||||||
|
run: coverage report --include pre_commit/languages/${{ matrix.language }}.py,tests/languages/${{ matrix.language }}_test.py
|
||||||
|
collector:
|
||||||
|
needs: [language]
|
||||||
|
if: always()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: check for failures
|
||||||
|
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
|
run: echo job failed && exit 1
|
||||||
23
.github/workflows/main.yml
vendored
Normal file
23
.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
name: main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, test-me-*]
|
||||||
|
tags: '*'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
main-windows:
|
||||||
|
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
|
||||||
|
with:
|
||||||
|
env: '["py310"]'
|
||||||
|
os: windows-latest
|
||||||
|
main-linux:
|
||||||
|
uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
|
||||||
|
with:
|
||||||
|
env: '["py310", "py311", "py312", "py313"]'
|
||||||
|
os: ubuntu-latest
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
|
|
@ -1,14 +1,6 @@
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.iml
|
|
||||||
*.py[co]
|
*.py[co]
|
||||||
.*.sw[a-z]
|
/.coverage
|
||||||
.coverage
|
/.tox
|
||||||
.idea
|
/dist
|
||||||
.project
|
.vscode/
|
||||||
.pydevproject
|
|
||||||
.tox
|
|
||||||
.venv.touch
|
|
||||||
/venv*
|
|
||||||
coverage-html
|
|
||||||
dist
|
|
||||||
.pytest_cache
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,44 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v1.2.3
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: autopep8-wrapper
|
|
||||||
- id: check-docstring-first
|
|
||||||
- id: check-json
|
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
|
- id: double-quote-string-fixer
|
||||||
- id: name-tests-test
|
- id: name-tests-test
|
||||||
- id: requirements-txt-fixer
|
- id: requirements-txt-fixer
|
||||||
- id: flake8
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
- repo: https://github.com/pre-commit/pre-commit
|
rev: v3.2.0
|
||||||
rev: v1.7.0
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: validate_manifest
|
- id: setup-cfg-fmt
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
rev: v1.0.1
|
rev: v3.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
language_version: python2.7
|
exclude: ^pre_commit/resources/
|
||||||
|
args: [--py310-plus, --add-import, 'from __future__ import annotations']
|
||||||
- repo: https://github.com/asottile/add-trailing-comma
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
rev: v0.6.4
|
rev: v4.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: add-trailing-comma
|
- id: add-trailing-comma
|
||||||
- repo: meta
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.21.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-hooks-apply
|
- id: pyupgrade
|
||||||
- id: check-useless-excludes
|
args: [--py310-plus]
|
||||||
|
- repo: https://github.com/hhatto/autopep8
|
||||||
|
rev: v2.3.2
|
||||||
|
hooks:
|
||||||
|
- id: autopep8
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 7.3.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
|
rev: v1.19.1
|
||||||
|
hooks:
|
||||||
|
- id: mypy
|
||||||
|
additional_dependencies: [types-pyyaml]
|
||||||
|
exclude: ^testing/resources/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
- id: validate_manifest
|
- id: validate_manifest
|
||||||
name: Validate Pre-Commit Manifest
|
name: validate pre-commit manifest
|
||||||
description: This validator validates a pre-commit hooks manifest file
|
description: This validator validates a pre-commit hooks manifest file
|
||||||
entry: pre-commit-validate-manifest
|
entry: pre-commit validate-manifest
|
||||||
language: python
|
language: python
|
||||||
files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$
|
files: ^\.pre-commit-hooks\.yaml$
|
||||||
|
|
|
||||||
37
.travis.yml
37
.travis.yml
|
|
@ -1,37 +0,0 @@
|
||||||
language: python
|
|
||||||
dist: trusty
|
|
||||||
sudo: required
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- env: TOXENV=py27
|
|
||||||
- env: TOXENV=py27 LATEST_GIT=1
|
|
||||||
- env: TOXENV=py36
|
|
||||||
python: 3.6
|
|
||||||
- env: TOXENV=pypy
|
|
||||||
python: pypy2.7-5.10.0
|
|
||||||
- env: TOXENV=py37
|
|
||||||
python: 3.7
|
|
||||||
sudo: required
|
|
||||||
dist: xenial
|
|
||||||
install: pip install coveralls tox
|
|
||||||
script: tox
|
|
||||||
before_install:
|
|
||||||
- git --version
|
|
||||||
- |
|
|
||||||
if [ "$LATEST_GIT" = "1" ]; then
|
|
||||||
testing/latest-git.sh
|
|
||||||
export PATH="/tmp/git/bin:$PATH"
|
|
||||||
fi
|
|
||||||
- git --version
|
|
||||||
- 'testing/get-swift.sh && export PATH="/tmp/swift/usr/bin:$PATH"'
|
|
||||||
- 'curl -sSf https://sh.rustup.rs | bash -s -- -y'
|
|
||||||
- export PATH="$HOME/.cargo/bin:$PATH"
|
|
||||||
after_success: coveralls
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.cache/pip
|
|
||||||
- $HOME/.cache/pre-commit
|
|
||||||
- $HOME/.rustup
|
|
||||||
- $HOME/.swift
|
|
||||||
2052
CHANGELOG.md
2052
CHANGELOG.md
File diff suppressed because it is too large
Load diff
120
CONTRIBUTING.md
120
CONTRIBUTING.md
|
|
@ -2,22 +2,26 @@
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
- The complete test suite depends on having at least the following installed (possibly not
|
- The complete test suite depends on having at least the following installed
|
||||||
a complete list)
|
(possibly not a complete list)
|
||||||
- git (A sufficiently newer version is required to run pre-push tests)
|
- git (Version 2.24.0 or above is required to run pre-merge-commit tests)
|
||||||
- python2 (Required by a test which checks different python versions)
|
|
||||||
- python3 (Required by a test which checks different python versions)
|
- python3 (Required by a test which checks different python versions)
|
||||||
- tox (or virtualenv)
|
- tox (or virtualenv)
|
||||||
- ruby + gem
|
- ruby + gem
|
||||||
- docker
|
- docker
|
||||||
|
- conda
|
||||||
|
- cargo (required by tests for rust dependencies)
|
||||||
|
- go (required by tests for go dependencies)
|
||||||
|
- swift
|
||||||
|
|
||||||
### Setting up an environemnt
|
### Setting up an environment
|
||||||
|
|
||||||
This is useful for running specific tests. The easiest way to set this up
|
This is useful for running specific tests. The easiest way to set this up
|
||||||
is to run:
|
is to run:
|
||||||
|
|
||||||
1. `tox -e venv`
|
1. `tox --devenv venv` (note: requires tox>=3.13)
|
||||||
2. `. venv-pre_commit/bin/activate`
|
2. `. venv/bin/activate` (or follow the [activation instructions] for your
|
||||||
|
platform)
|
||||||
|
|
||||||
This will create and put you into a virtualenv which has an editable
|
This will create and put you into a virtualenv which has an editable
|
||||||
installation of pre-commit. Hack away! Running `pre-commit` will reflect
|
installation of pre-commit. Hack away! Running `pre-commit` will reflect
|
||||||
|
|
@ -30,7 +34,7 @@ Running a specific test with the environment activated is as easy as:
|
||||||
|
|
||||||
### Running all the tests
|
### Running all the tests
|
||||||
|
|
||||||
Running all the tests can be done by running `tox -e py27` (or your
|
Running all the tests can be done by running `tox -e py37` (or your
|
||||||
interpreter version of choice). These often take a long time and consume
|
interpreter version of choice). These often take a long time and consume
|
||||||
significant cpu while running the slower node / ruby integration tests.
|
significant cpu while running the slower node / ruby integration tests.
|
||||||
|
|
||||||
|
|
@ -49,5 +53,101 @@ Documentation is hosted at https://pre-commit.com
|
||||||
This website is controlled through
|
This website is controlled through
|
||||||
https://github.com/pre-commit/pre-commit.github.io
|
https://github.com/pre-commit/pre-commit.github.io
|
||||||
|
|
||||||
When adding a feature, please make a pull request to add yourself to the
|
## Adding support for a new hook language
|
||||||
contributors list and add documentation to the website if applicable.
|
|
||||||
|
pre-commit already supports many [programming languages](https://pre-commit.com/#supported-languages)
|
||||||
|
to write hook executables with.
|
||||||
|
|
||||||
|
When adding support for a language, you must first decide what level of support
|
||||||
|
to implement. The current implemented languages are at varying levels:
|
||||||
|
|
||||||
|
- 0th class - pre-commit does not require any dependencies for these languages
|
||||||
|
as they're not actually languages (current examples: fail, pygrep)
|
||||||
|
- 1st class - pre-commit will bootstrap a full interpreter requiring nothing to
|
||||||
|
be installed globally (current examples: go, node, ruby, rust)
|
||||||
|
- 2nd class - pre-commit requires the user to install the language globally but
|
||||||
|
will install tools in an isolated fashion (current examples: python, swift,
|
||||||
|
docker).
|
||||||
|
- 3rd class - pre-commit requires the user to install both the tool and the
|
||||||
|
language globally (current examples: script, system)
|
||||||
|
|
||||||
|
"second class" is usually the easiest to implement first and is perfectly
|
||||||
|
acceptable.
|
||||||
|
|
||||||
|
Ideally the language works on the supported platforms for pre-commit (linux,
|
||||||
|
windows, macos) but it's ok to skip one or more platforms (for example, swift
|
||||||
|
doesn't run on windows).
|
||||||
|
|
||||||
|
When writing your new language, it's often useful to look at other examples in
|
||||||
|
the `pre_commit/languages` directory.
|
||||||
|
|
||||||
|
It might also be useful to look at a recent pull request which added a
|
||||||
|
language, for example:
|
||||||
|
|
||||||
|
- [rust](https://github.com/pre-commit/pre-commit/pull/751)
|
||||||
|
- [fail](https://github.com/pre-commit/pre-commit/pull/812)
|
||||||
|
- [swift](https://github.com/pre-commit/pre-commit/pull/467)
|
||||||
|
|
||||||
|
### `language` api
|
||||||
|
|
||||||
|
here are the apis that should be implemented for a language
|
||||||
|
|
||||||
|
Note that these are also documented in [`pre_commit/lang_base.py`](https://github.com/pre-commit/pre-commit/blob/main/pre_commit/lang_base.py)
|
||||||
|
|
||||||
|
#### `ENVIRONMENT_DIR`
|
||||||
|
|
||||||
|
a short string which will be used for the prefix of where packages will be
|
||||||
|
installed. For example, python uses `py_env` and installs a `virtualenv` at
|
||||||
|
that location.
|
||||||
|
|
||||||
|
this will be `None` for 0th / 3rd class languages as they don't have an install
|
||||||
|
step.
|
||||||
|
|
||||||
|
#### `get_default_version`
|
||||||
|
|
||||||
|
This is used to retrieve the default `language_version` for a language. If
|
||||||
|
one cannot be determined, return `'default'`.
|
||||||
|
|
||||||
|
You generally don't need to implement this on a first pass and can just use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
get_default_version = lang_base.basic_default_version
|
||||||
|
```
|
||||||
|
|
||||||
|
`python` is currently the only language which implements this api
|
||||||
|
|
||||||
|
#### `health_check`
|
||||||
|
|
||||||
|
This is used to check whether the installed environment is considered healthy.
|
||||||
|
This function should return a detailed message if unhealthy or `None` if
|
||||||
|
healthy.
|
||||||
|
|
||||||
|
You generally don't need to implement this on a first pass and can just use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
```
|
||||||
|
|
||||||
|
`python` is currently the only language which implements this api, for python
|
||||||
|
it is checking whether some common dlls are still available.
|
||||||
|
|
||||||
|
#### `install_environment`
|
||||||
|
|
||||||
|
this is the trickiest one to implement and where all the smart parts happen.
|
||||||
|
|
||||||
|
this api should do the following things
|
||||||
|
|
||||||
|
- (0th / 3rd class): `install_environment = lang_base.no_install`
|
||||||
|
- (1st class): install a language runtime into the hook's directory
|
||||||
|
- (2nd class): install the package at `.` into the `ENVIRONMENT_DIR`
|
||||||
|
- (2nd class, optional): install packages listed in `additional_dependencies`
|
||||||
|
into `ENVIRONMENT_DIR` (not a required feature for a first pass)
|
||||||
|
|
||||||
|
#### `run_hook`
|
||||||
|
|
||||||
|
This is usually the easiest to implement, most of them look the same as the
|
||||||
|
`node` hook implementation:
|
||||||
|
|
||||||
|
https://github.com/pre-commit/pre-commit/blob/160238220f022035c8ef869c9a8642f622c02118/pre_commit/languages/node.py#L72-L74
|
||||||
|
|
||||||
|
[activation instructions]: https://virtualenv.pypa.io/en/latest/user_guide.html#activators
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
include LICENSE
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
[](https://travis-ci.org/pre-commit/pre-commit)
|
[](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml)
|
||||||
[](https://coveralls.io/github/pre-commit/pre-commit?branch=master)
|
[](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main)
|
||||||
[](https://ci.appveyor.com/project/asottile/pre-commit/branch/master)
|
|
||||||
|
|
||||||
## pre-commit
|
## pre-commit
|
||||||
|
|
||||||
|
|
|
||||||
29
appveyor.yml
29
appveyor.yml
|
|
@ -1,29 +0,0 @@
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
COVERAGE_IGNORE_WINDOWS: '# pragma: windows no cover'
|
|
||||||
TOX_TESTENV_PASSENV: COVERAGE_IGNORE_WINDOWS
|
|
||||||
matrix:
|
|
||||||
- TOXENV: py27
|
|
||||||
- TOXENV: py37
|
|
||||||
|
|
||||||
install:
|
|
||||||
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%"
|
|
||||||
- pip install tox virtualenv --upgrade
|
|
||||||
- "mkdir -p C:\\Temp"
|
|
||||||
- "SET TMPDIR=C:\\Temp"
|
|
||||||
- "curl -sSf https://sh.rustup.rs | bash -s -- -y"
|
|
||||||
- "SET PATH=%USERPROFILE%\\.cargo\\bin;%PATH%"
|
|
||||||
|
|
||||||
# Not a C# project
|
|
||||||
build: false
|
|
||||||
|
|
||||||
before_test:
|
|
||||||
# Shut up CRLF messages
|
|
||||||
- git config --global core.autocrlf false
|
|
||||||
- git config --global core.safecrlf false
|
|
||||||
|
|
||||||
test_script: tox
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- '%LOCALAPPDATA%\pip\cache'
|
|
||||||
- '%USERPROFILE%\.cache\pre-commit'
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
|
|
||||||
from pre_commit.main import main
|
from pre_commit.main import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
50
pre_commit/all_languages.py
Normal file
50
pre_commit/all_languages.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pre_commit.lang_base import Language
|
||||||
|
from pre_commit.languages import conda
|
||||||
|
from pre_commit.languages import coursier
|
||||||
|
from pre_commit.languages import dart
|
||||||
|
from pre_commit.languages import docker
|
||||||
|
from pre_commit.languages import docker_image
|
||||||
|
from pre_commit.languages import dotnet
|
||||||
|
from pre_commit.languages import fail
|
||||||
|
from pre_commit.languages import golang
|
||||||
|
from pre_commit.languages import haskell
|
||||||
|
from pre_commit.languages import julia
|
||||||
|
from pre_commit.languages import lua
|
||||||
|
from pre_commit.languages import node
|
||||||
|
from pre_commit.languages import perl
|
||||||
|
from pre_commit.languages import pygrep
|
||||||
|
from pre_commit.languages import python
|
||||||
|
from pre_commit.languages import r
|
||||||
|
from pre_commit.languages import ruby
|
||||||
|
from pre_commit.languages import rust
|
||||||
|
from pre_commit.languages import swift
|
||||||
|
from pre_commit.languages import unsupported
|
||||||
|
from pre_commit.languages import unsupported_script
|
||||||
|
|
||||||
|
|
||||||
|
languages: dict[str, Language] = {
|
||||||
|
'conda': conda,
|
||||||
|
'coursier': coursier,
|
||||||
|
'dart': dart,
|
||||||
|
'docker': docker,
|
||||||
|
'docker_image': docker_image,
|
||||||
|
'dotnet': dotnet,
|
||||||
|
'fail': fail,
|
||||||
|
'golang': golang,
|
||||||
|
'haskell': haskell,
|
||||||
|
'julia': julia,
|
||||||
|
'lua': lua,
|
||||||
|
'node': node,
|
||||||
|
'perl': perl,
|
||||||
|
'pygrep': pygrep,
|
||||||
|
'python': python,
|
||||||
|
'r': r,
|
||||||
|
'ruby': ruby,
|
||||||
|
'rust': rust,
|
||||||
|
'swift': swift,
|
||||||
|
'unsupported': unsupported,
|
||||||
|
'unsupported_script': unsupported_script,
|
||||||
|
}
|
||||||
|
language_names = sorted(languages)
|
||||||
|
|
@ -1,49 +1,252 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import collections
|
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
import cfgv
|
import cfgv
|
||||||
from aspy.yaml import ordered_load
|
|
||||||
from identify.identify import ALL_TAGS
|
from identify.identify import ALL_TAGS
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit.error_handler import FatalError
|
from pre_commit.all_languages import language_names
|
||||||
from pre_commit.languages.all import all_languages
|
from pre_commit.errors import FatalError
|
||||||
|
from pre_commit.yaml import yaml_load
|
||||||
|
|
||||||
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
|
check_string_regex = cfgv.check_and(cfgv.check_string, cfgv.check_regex)
|
||||||
|
|
||||||
|
HOOK_TYPES = (
|
||||||
|
'commit-msg',
|
||||||
|
'post-checkout',
|
||||||
|
'post-commit',
|
||||||
|
'post-merge',
|
||||||
|
'post-rewrite',
|
||||||
|
'pre-commit',
|
||||||
|
'pre-merge-commit',
|
||||||
|
'pre-push',
|
||||||
|
'pre-rebase',
|
||||||
|
'prepare-commit-msg',
|
||||||
|
)
|
||||||
|
# `manual` is not invoked by any installed git hook. See #719
|
||||||
|
STAGES = (*HOOK_TYPES, 'manual')
|
||||||
|
|
||||||
|
|
||||||
def check_type_tag(tag):
|
def check_type_tag(tag: str) -> None:
|
||||||
if tag not in ALL_TAGS:
|
if tag not in ALL_TAGS:
|
||||||
raise cfgv.ValidationError(
|
raise cfgv.ValidationError(
|
||||||
'Type tag {!r} is not recognized. '
|
f'Type tag {tag!r} is not recognized. '
|
||||||
'Try upgrading identify and pre-commit?'.format(tag),
|
f'Try upgrading identify and pre-commit?',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_argparser(filenames_help):
|
def parse_version(s: str) -> tuple[int, ...]:
|
||||||
parser = argparse.ArgumentParser()
|
"""poor man's version comparison"""
|
||||||
parser.add_argument('filenames', nargs='*', help=filenames_help)
|
return tuple(int(p) for p in s.split('.'))
|
||||||
parser.add_argument('-V', '--version', action='version', version=C.VERSION)
|
|
||||||
return parser
|
|
||||||
|
def check_min_version(version: str) -> None:
|
||||||
|
if parse_version(version) > parse_version(C.VERSION):
|
||||||
|
raise cfgv.ValidationError(
|
||||||
|
f'pre-commit version {version} is required but version '
|
||||||
|
f'{C.VERSION} is installed. '
|
||||||
|
f'Perhaps run `pip install --upgrade pre-commit`.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_STAGES = {
|
||||||
|
'commit': 'pre-commit',
|
||||||
|
'merge-commit': 'pre-merge-commit',
|
||||||
|
'push': 'pre-push',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def transform_stage(stage: str) -> str:
|
||||||
|
return _STAGES.get(stage, stage)
|
||||||
|
|
||||||
|
|
||||||
|
MINIMAL_MANIFEST_SCHEMA = cfgv.Array(
|
||||||
|
cfgv.Map(
|
||||||
|
'Hook', 'id',
|
||||||
|
cfgv.Required('id', cfgv.check_string),
|
||||||
|
cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def warn_for_stages_on_repo_init(repo: str, directory: str) -> None:
|
||||||
|
try:
|
||||||
|
manifest = cfgv.load_from_filename(
|
||||||
|
os.path.join(directory, C.MANIFEST_FILE),
|
||||||
|
schema=MINIMAL_MANIFEST_SCHEMA,
|
||||||
|
load_strategy=yaml_load,
|
||||||
|
exc_tp=InvalidManifestError,
|
||||||
|
)
|
||||||
|
except InvalidManifestError:
|
||||||
|
return # they'll get a better error message when it actually loads!
|
||||||
|
|
||||||
|
legacy_stages = {} # sorted set
|
||||||
|
for hook in manifest:
|
||||||
|
for stage in hook.get('stages', ()):
|
||||||
|
if stage in _STAGES:
|
||||||
|
legacy_stages[stage] = True
|
||||||
|
|
||||||
|
if legacy_stages:
|
||||||
|
logger.warning(
|
||||||
|
f'repo `{repo}` uses deprecated stage names '
|
||||||
|
f'({", ".join(legacy_stages)}) which will be removed in a '
|
||||||
|
f'future version. '
|
||||||
|
f'Hint: often `pre-commit autoupdate --repo {shlex.quote(repo)}` '
|
||||||
|
f'will fix this. '
|
||||||
|
f'if it does not -- consider reporting an issue to that repo.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StagesMigrationNoDefault(NamedTuple):
|
||||||
|
key: str
|
||||||
|
default: Sequence[str]
|
||||||
|
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
|
||||||
|
with cfgv.validate_context(f'At key: {self.key}'):
|
||||||
|
val = dct[self.key]
|
||||||
|
cfgv.check_array(cfgv.check_any)(val)
|
||||||
|
|
||||||
|
val = [transform_stage(v) for v in val]
|
||||||
|
cfgv.check_array(cfgv.check_one_of(STAGES))(val)
|
||||||
|
|
||||||
|
def apply_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
dct[self.key] = [transform_stage(v) for v in dct[self.key]]
|
||||||
|
|
||||||
|
def remove_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class StagesMigration(StagesMigrationNoDefault):
|
||||||
|
def apply_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
dct.setdefault(self.key, self.default)
|
||||||
|
super().apply_default(dct)
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedStagesWarning(NamedTuple):
|
||||||
|
key: str
|
||||||
|
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
|
||||||
|
val = dct[self.key]
|
||||||
|
cfgv.check_array(cfgv.check_any)(val)
|
||||||
|
|
||||||
|
legacy_stages = [stage for stage in val if stage in _STAGES]
|
||||||
|
if legacy_stages:
|
||||||
|
logger.warning(
|
||||||
|
f'hook id `{dct["id"]}` uses deprecated stage names '
|
||||||
|
f'({", ".join(legacy_stages)}) which will be removed in a '
|
||||||
|
f'future version. '
|
||||||
|
f'run: `pre-commit migrate-config` to automatically fix this.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedDefaultStagesWarning(NamedTuple):
|
||||||
|
key: str
|
||||||
|
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
|
||||||
|
val = dct[self.key]
|
||||||
|
cfgv.check_array(cfgv.check_any)(val)
|
||||||
|
|
||||||
|
legacy_stages = [stage for stage in val if stage in _STAGES]
|
||||||
|
if legacy_stages:
|
||||||
|
logger.warning(
|
||||||
|
f'top-level `default_stages` uses deprecated stage names '
|
||||||
|
f'({", ".join(legacy_stages)}) which will be removed in a '
|
||||||
|
f'future version. '
|
||||||
|
f'run: `pre-commit migrate-config` to automatically fix this.',
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_language(name: str) -> str:
|
||||||
|
return {
|
||||||
|
'system': 'unsupported',
|
||||||
|
'script': 'unsupported_script',
|
||||||
|
}.get(name, name)
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageMigration(NamedTuple): # remove
|
||||||
|
key: str
|
||||||
|
check_fn: Callable[[object], None]
|
||||||
|
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
|
||||||
|
with cfgv.validate_context(f'At key: {self.key}'):
|
||||||
|
self.check_fn(_translate_language(dct[self.key]))
|
||||||
|
|
||||||
|
def apply_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
return
|
||||||
|
|
||||||
|
dct[self.key] = _translate_language(dct[self.key])
|
||||||
|
|
||||||
|
def remove_default(self, dct: dict[str, Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class LanguageMigrationRequired(LanguageMigration): # replace with Required
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key not in dct:
|
||||||
|
raise cfgv.ValidationError(f'Missing required key: {self.key}')
|
||||||
|
|
||||||
|
super().check(dct)
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_HOOK_DICT = cfgv.Map(
|
MANIFEST_HOOK_DICT = cfgv.Map(
|
||||||
'Hook', 'id',
|
'Hook', 'id',
|
||||||
|
|
||||||
|
# check first in case it uses some newer, incompatible feature
|
||||||
|
cfgv.Optional(
|
||||||
|
'minimum_pre_commit_version',
|
||||||
|
cfgv.check_and(cfgv.check_string, check_min_version),
|
||||||
|
'0',
|
||||||
|
),
|
||||||
|
|
||||||
cfgv.Required('id', cfgv.check_string),
|
cfgv.Required('id', cfgv.check_string),
|
||||||
cfgv.Required('name', cfgv.check_string),
|
cfgv.Required('name', cfgv.check_string),
|
||||||
cfgv.Required('entry', cfgv.check_string),
|
cfgv.Required('entry', cfgv.check_string),
|
||||||
cfgv.Required('language', cfgv.check_one_of(all_languages)),
|
LanguageMigrationRequired('language', cfgv.check_one_of(language_names)),
|
||||||
|
cfgv.Optional('alias', cfgv.check_string, ''),
|
||||||
|
|
||||||
cfgv.Optional(
|
cfgv.Optional('files', check_string_regex, ''),
|
||||||
'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '',
|
cfgv.Optional('exclude', check_string_regex, '^$'),
|
||||||
),
|
|
||||||
cfgv.Optional(
|
|
||||||
'exclude', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '^$',
|
|
||||||
),
|
|
||||||
cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']),
|
cfgv.Optional('types', cfgv.check_array(check_type_tag), ['file']),
|
||||||
|
cfgv.Optional('types_or', cfgv.check_array(check_type_tag), []),
|
||||||
cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []),
|
cfgv.Optional('exclude_types', cfgv.check_array(check_type_tag), []),
|
||||||
|
|
||||||
cfgv.Optional(
|
cfgv.Optional(
|
||||||
|
|
@ -51,12 +254,13 @@ MANIFEST_HOOK_DICT = cfgv.Map(
|
||||||
),
|
),
|
||||||
cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []),
|
cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []),
|
||||||
cfgv.Optional('always_run', cfgv.check_bool, False),
|
cfgv.Optional('always_run', cfgv.check_bool, False),
|
||||||
|
cfgv.Optional('fail_fast', cfgv.check_bool, False),
|
||||||
cfgv.Optional('pass_filenames', cfgv.check_bool, True),
|
cfgv.Optional('pass_filenames', cfgv.check_bool, True),
|
||||||
cfgv.Optional('description', cfgv.check_string, ''),
|
cfgv.Optional('description', cfgv.check_string, ''),
|
||||||
cfgv.Optional('language_version', cfgv.check_string, 'default'),
|
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
|
||||||
cfgv.Optional('log_file', cfgv.check_string, ''),
|
cfgv.Optional('log_file', cfgv.check_string, ''),
|
||||||
cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
|
cfgv.Optional('require_serial', cfgv.check_bool, False),
|
||||||
cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
|
StagesMigration('stages', []),
|
||||||
cfgv.Optional('verbose', cfgv.check_bool, False),
|
cfgv.Optional('verbose', cfgv.check_bool, False),
|
||||||
)
|
)
|
||||||
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
|
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
|
||||||
|
|
@ -66,60 +270,172 @@ class InvalidManifestError(FatalError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _load_manifest_forward_compat(contents: str) -> object:
|
||||||
|
obj = yaml_load(contents)
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
check_min_version('5')
|
||||||
|
raise AssertionError('unreachable')
|
||||||
|
else:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
load_manifest = functools.partial(
|
load_manifest = functools.partial(
|
||||||
cfgv.load_from_filename,
|
cfgv.load_from_filename,
|
||||||
schema=MANIFEST_SCHEMA,
|
schema=MANIFEST_SCHEMA,
|
||||||
load_strategy=ordered_load,
|
load_strategy=_load_manifest_forward_compat,
|
||||||
exc_tp=InvalidManifestError,
|
exc_tp=InvalidManifestError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_manifest_main(argv=None):
|
LOCAL = 'local'
|
||||||
parser = _make_argparser('Manifest filenames.')
|
META = 'meta'
|
||||||
args = parser.parse_args(argv)
|
|
||||||
ret = 0
|
|
||||||
for filename in args.filenames:
|
|
||||||
try:
|
|
||||||
load_manifest(filename)
|
|
||||||
except InvalidManifestError as e:
|
|
||||||
print(e)
|
|
||||||
ret = 1
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
_LOCAL_SENTINEL = 'local'
|
class WarnMutableRev(cfgv.Conditional):
|
||||||
_META_SENTINEL = 'meta'
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
super().check(dct)
|
||||||
|
|
||||||
|
if self.key in dct:
|
||||||
|
rev = dct[self.key]
|
||||||
|
|
||||||
|
if '.' not in rev and not re.match(r'^[a-fA-F0-9]+$', rev):
|
||||||
|
logger.warning(
|
||||||
|
f'The {self.key!r} field of repo {dct["repo"]!r} '
|
||||||
|
f'appears to be a mutable reference '
|
||||||
|
f'(moving tag / branch). Mutable references are never '
|
||||||
|
f'updated after first install and are not supported. '
|
||||||
|
f'See https://pre-commit.com/#using-the-latest-version-for-a-repository ' # noqa: E501
|
||||||
|
f'for more details. '
|
||||||
|
f'Hint: `pre-commit autoupdate` often fixes this.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MigrateShaToRev(object):
|
class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault):
|
||||||
@staticmethod
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
def _cond(key):
|
super().check(dct)
|
||||||
return cfgv.Conditional(
|
|
||||||
key, cfgv.check_string,
|
|
||||||
condition_key='repo',
|
|
||||||
condition_value=cfgv.NotIn(_LOCAL_SENTINEL, _META_SENTINEL),
|
|
||||||
ensure_absent=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def check(self, dct):
|
if '/*' in dct.get(self.key, ''):
|
||||||
if dct.get('repo') in {_LOCAL_SENTINEL, _META_SENTINEL}:
|
logger.warning(
|
||||||
self._cond('rev').check(dct)
|
f'The {self.key!r} field in hook {dct.get("id")!r} is a '
|
||||||
self._cond('sha').check(dct)
|
f"regex, not a glob -- matching '/*' probably isn't what you "
|
||||||
elif 'sha' in dct and 'rev' in dct:
|
f'want here',
|
||||||
raise cfgv.ValidationError('Cannot specify both sha and rev')
|
)
|
||||||
elif 'sha' in dct:
|
for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'):
|
||||||
self._cond('sha').check(dct)
|
if fwd_slash_re in dct.get(self.key, ''):
|
||||||
else:
|
logger.warning(
|
||||||
self._cond('rev').check(dct)
|
fr'pre-commit normalizes slashes in the {self.key!r} '
|
||||||
|
fr'field in hook {dct.get("id")!r} to forward slashes, '
|
||||||
def apply_default(self, dct):
|
fr'so you can use / instead of {fwd_slash_re}',
|
||||||
if 'sha' in dct:
|
)
|
||||||
dct['rev'] = dct.pop('sha')
|
|
||||||
|
|
||||||
def remove_default(self, dct):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalSensibleRegexAtTop(cfgv.OptionalNoDefault):
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
super().check(dct)
|
||||||
|
|
||||||
|
if '/*' in dct.get(self.key, ''):
|
||||||
|
logger.warning(
|
||||||
|
f'The top-level {self.key!r} field is a regex, not a glob -- '
|
||||||
|
f"matching '/*' probably isn't what you want here",
|
||||||
|
)
|
||||||
|
for fwd_slash_re in (r'[\\/]', r'[\/]', r'[/\\]'):
|
||||||
|
if fwd_slash_re in dct.get(self.key, ''):
|
||||||
|
logger.warning(
|
||||||
|
fr'pre-commit normalizes the slashes in the top-level '
|
||||||
|
fr'{self.key!r} field to forward slashes, so you '
|
||||||
|
fr'can use / instead of {fwd_slash_re}',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _entry(modname: str) -> str:
|
||||||
|
"""the hook `entry` is passed through `shlex.split()` by the command
|
||||||
|
runner, so to prevent issues with spaces and backslashes (on Windows)
|
||||||
|
it must be quoted here.
|
||||||
|
"""
|
||||||
|
return f'{shlex.quote(sys.executable)} -m pre_commit.meta_hooks.{modname}'
|
||||||
|
|
||||||
|
|
||||||
|
def warn_unknown_keys_root(
|
||||||
|
extra: Sequence[str],
|
||||||
|
orig_keys: Sequence[str],
|
||||||
|
dct: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}')
|
||||||
|
|
||||||
|
|
||||||
|
def warn_unknown_keys_repo(
|
||||||
|
extra: Sequence[str],
|
||||||
|
orig_keys: Sequence[str],
|
||||||
|
dct: dict[str, str],
|
||||||
|
) -> None:
|
||||||
|
logger.warning(
|
||||||
|
f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_meta = (
|
||||||
|
(
|
||||||
|
'check-hooks-apply', (
|
||||||
|
('name', 'Check hooks apply to the repository'),
|
||||||
|
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
|
||||||
|
('entry', _entry('check_hooks_apply')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'check-useless-excludes', (
|
||||||
|
('name', 'Check for useless excludes'),
|
||||||
|
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
|
||||||
|
('entry', _entry('check_useless_excludes')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'identity', (
|
||||||
|
('name', 'identity'),
|
||||||
|
('verbose', True),
|
||||||
|
('entry', _entry('identity')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotAllowed(cfgv.OptionalNoDefault):
|
||||||
|
def check(self, dct: dict[str, Any]) -> None:
|
||||||
|
if self.key in dct:
|
||||||
|
raise cfgv.ValidationError(f'{self.key!r} cannot be overridden')
|
||||||
|
|
||||||
|
|
||||||
|
_COMMON_HOOK_WARNINGS = (
|
||||||
|
OptionalSensibleRegexAtHook('files', cfgv.check_string),
|
||||||
|
OptionalSensibleRegexAtHook('exclude', cfgv.check_string),
|
||||||
|
DeprecatedStagesWarning('stages'),
|
||||||
|
)
|
||||||
|
|
||||||
|
META_HOOK_DICT = cfgv.Map(
|
||||||
|
'Hook', 'id',
|
||||||
|
cfgv.Required('id', cfgv.check_string),
|
||||||
|
cfgv.Required('id', cfgv.check_one_of(tuple(k for k, _ in _meta))),
|
||||||
|
# language must be `unsupported`
|
||||||
|
cfgv.Optional(
|
||||||
|
'language', cfgv.check_one_of({'unsupported'}), 'unsupported',
|
||||||
|
),
|
||||||
|
# entry cannot be overridden
|
||||||
|
NotAllowed('entry', cfgv.check_any),
|
||||||
|
*(
|
||||||
|
# default to the hook definition for the meta hooks
|
||||||
|
cfgv.ConditionalOptional(key, cfgv.check_any, value, 'id', hook_id)
|
||||||
|
for hook_id, values in _meta
|
||||||
|
for key, value in values
|
||||||
|
),
|
||||||
|
*(
|
||||||
|
# default to the "manifest" parsing
|
||||||
|
cfgv.OptionalNoDefault(item.key, item.check_fn)
|
||||||
|
# these will always be defaulted above
|
||||||
|
if item.key in {'name', 'language', 'entry'} else
|
||||||
|
item
|
||||||
|
for item in MANIFEST_HOOK_DICT.items
|
||||||
|
),
|
||||||
|
*_COMMON_HOOK_WARNINGS,
|
||||||
|
)
|
||||||
CONFIG_HOOK_DICT = cfgv.Map(
|
CONFIG_HOOK_DICT = cfgv.Map(
|
||||||
'Hook', 'id',
|
'Hook', 'id',
|
||||||
|
|
||||||
|
|
@ -129,66 +445,107 @@ CONFIG_HOOK_DICT = cfgv.Map(
|
||||||
# are optional.
|
# are optional.
|
||||||
# No defaults are provided here as the config is merged on top of the
|
# No defaults are provided here as the config is merged on top of the
|
||||||
# manifest.
|
# manifest.
|
||||||
*[
|
*(
|
||||||
cfgv.OptionalNoDefault(item.key, item.check_fn)
|
cfgv.OptionalNoDefault(item.key, item.check_fn)
|
||||||
for item in MANIFEST_HOOK_DICT.items
|
for item in MANIFEST_HOOK_DICT.items
|
||||||
if item.key != 'id'
|
if item.key != 'id'
|
||||||
]
|
if item.key != 'stages'
|
||||||
|
if item.key != 'language' # remove
|
||||||
|
),
|
||||||
|
StagesMigrationNoDefault('stages', []),
|
||||||
|
LanguageMigration('language', cfgv.check_one_of(language_names)), # remove
|
||||||
|
*_COMMON_HOOK_WARNINGS,
|
||||||
|
)
|
||||||
|
LOCAL_HOOK_DICT = cfgv.Map(
|
||||||
|
'Hook', 'id',
|
||||||
|
|
||||||
|
*MANIFEST_HOOK_DICT.items,
|
||||||
|
*_COMMON_HOOK_WARNINGS,
|
||||||
)
|
)
|
||||||
CONFIG_REPO_DICT = cfgv.Map(
|
CONFIG_REPO_DICT = cfgv.Map(
|
||||||
'Repository', 'repo',
|
'Repository', 'repo',
|
||||||
|
|
||||||
cfgv.Required('repo', cfgv.check_string),
|
cfgv.Required('repo', cfgv.check_string),
|
||||||
cfgv.RequiredRecurse('hooks', cfgv.Array(CONFIG_HOOK_DICT)),
|
|
||||||
|
|
||||||
MigrateShaToRev(),
|
cfgv.ConditionalRecurse(
|
||||||
|
'hooks', cfgv.Array(CONFIG_HOOK_DICT),
|
||||||
|
'repo', cfgv.NotIn(LOCAL, META),
|
||||||
|
),
|
||||||
|
cfgv.ConditionalRecurse(
|
||||||
|
'hooks', cfgv.Array(LOCAL_HOOK_DICT),
|
||||||
|
'repo', LOCAL,
|
||||||
|
),
|
||||||
|
cfgv.ConditionalRecurse(
|
||||||
|
'hooks', cfgv.Array(META_HOOK_DICT),
|
||||||
|
'repo', META,
|
||||||
|
),
|
||||||
|
|
||||||
|
WarnMutableRev(
|
||||||
|
'rev', cfgv.check_string,
|
||||||
|
condition_key='repo',
|
||||||
|
condition_value=cfgv.NotIn(LOCAL, META),
|
||||||
|
ensure_absent=True,
|
||||||
|
),
|
||||||
|
cfgv.WarnAdditionalKeys(('repo', 'rev', 'hooks'), warn_unknown_keys_repo),
|
||||||
|
)
|
||||||
|
DEFAULT_LANGUAGE_VERSION = cfgv.Map(
|
||||||
|
'DefaultLanguageVersion', None,
|
||||||
|
cfgv.NoAdditionalKeys(language_names),
|
||||||
|
*(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names),
|
||||||
)
|
)
|
||||||
CONFIG_SCHEMA = cfgv.Map(
|
CONFIG_SCHEMA = cfgv.Map(
|
||||||
'Config', None,
|
'Config', None,
|
||||||
|
|
||||||
|
# check first in case it uses some newer, incompatible feature
|
||||||
|
cfgv.Optional(
|
||||||
|
'minimum_pre_commit_version',
|
||||||
|
cfgv.check_and(cfgv.check_string, check_min_version),
|
||||||
|
'0',
|
||||||
|
),
|
||||||
|
|
||||||
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
|
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
|
||||||
cfgv.Optional('exclude', cfgv.check_regex, '^$'),
|
cfgv.Optional(
|
||||||
|
'default_install_hook_types',
|
||||||
|
cfgv.check_array(cfgv.check_one_of(HOOK_TYPES)),
|
||||||
|
['pre-commit'],
|
||||||
|
),
|
||||||
|
cfgv.OptionalRecurse(
|
||||||
|
'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
|
||||||
|
),
|
||||||
|
StagesMigration('default_stages', STAGES),
|
||||||
|
DeprecatedDefaultStagesWarning('default_stages'),
|
||||||
|
cfgv.Optional('files', check_string_regex, ''),
|
||||||
|
cfgv.Optional('exclude', check_string_regex, '^$'),
|
||||||
cfgv.Optional('fail_fast', cfgv.check_bool, False),
|
cfgv.Optional('fail_fast', cfgv.check_bool, False),
|
||||||
|
cfgv.WarnAdditionalKeys(
|
||||||
|
(
|
||||||
|
'repos',
|
||||||
|
'default_install_hook_types',
|
||||||
|
'default_language_version',
|
||||||
|
'default_stages',
|
||||||
|
'files',
|
||||||
|
'exclude',
|
||||||
|
'fail_fast',
|
||||||
|
'minimum_pre_commit_version',
|
||||||
|
'ci',
|
||||||
|
),
|
||||||
|
warn_unknown_keys_root,
|
||||||
|
),
|
||||||
|
OptionalSensibleRegexAtTop('files', cfgv.check_string),
|
||||||
|
OptionalSensibleRegexAtTop('exclude', cfgv.check_string),
|
||||||
|
|
||||||
|
# do not warn about configuration for pre-commit.ci
|
||||||
|
cfgv.OptionalNoDefault('ci', cfgv.check_type(dict)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_local_repo(repo_entry):
|
|
||||||
return repo_entry['repo'] == _LOCAL_SENTINEL
|
|
||||||
|
|
||||||
|
|
||||||
def is_meta_repo(repo_entry):
|
|
||||||
return repo_entry['repo'] == _META_SENTINEL
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidConfigError(FatalError):
|
class InvalidConfigError(FatalError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def ordered_load_normalize_legacy_config(contents):
|
|
||||||
data = ordered_load(contents)
|
|
||||||
if isinstance(data, list):
|
|
||||||
# TODO: Once happy, issue a deprecation warning and instructions
|
|
||||||
return collections.OrderedDict([('repos', data)])
|
|
||||||
else:
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
load_config = functools.partial(
|
load_config = functools.partial(
|
||||||
cfgv.load_from_filename,
|
cfgv.load_from_filename,
|
||||||
schema=CONFIG_SCHEMA,
|
schema=CONFIG_SCHEMA,
|
||||||
load_strategy=ordered_load_normalize_legacy_config,
|
load_strategy=yaml_load,
|
||||||
exc_tp=InvalidConfigError,
|
exc_tp=InvalidConfigError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_config_main(argv=None):
|
|
||||||
parser = _make_argparser('Config filenames.')
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
ret = 0
|
|
||||||
for filename in args.filenames:
|
|
||||||
try:
|
|
||||||
load_config(filename)
|
|
||||||
except InvalidConfigError as e:
|
|
||||||
print(e)
|
|
||||||
ret = 1
|
|
||||||
return ret
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,70 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if os.name == 'nt': # pragma: no cover (windows)
|
if sys.platform == 'win32': # pragma: no cover (windows)
|
||||||
from pre_commit.color_windows import enable_virtual_terminal_processing
|
def _enable() -> None:
|
||||||
|
from ctypes import POINTER
|
||||||
|
from ctypes import windll
|
||||||
|
from ctypes import WinError
|
||||||
|
from ctypes import WINFUNCTYPE
|
||||||
|
from ctypes.wintypes import BOOL
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
from ctypes.wintypes import HANDLE
|
||||||
|
|
||||||
|
STD_ERROR_HANDLE = -12
|
||||||
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
|
||||||
|
|
||||||
|
def bool_errcheck(result, func, args):
|
||||||
|
if not result:
|
||||||
|
raise WinError()
|
||||||
|
return args
|
||||||
|
|
||||||
|
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
|
||||||
|
('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),),
|
||||||
|
)
|
||||||
|
|
||||||
|
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
|
||||||
|
('GetConsoleMode', windll.kernel32),
|
||||||
|
((1, 'hConsoleHandle'), (2, 'lpMode')),
|
||||||
|
)
|
||||||
|
GetConsoleMode.errcheck = bool_errcheck
|
||||||
|
|
||||||
|
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
|
||||||
|
('SetConsoleMode', windll.kernel32),
|
||||||
|
((1, 'hConsoleHandle'), (1, 'dwMode')),
|
||||||
|
)
|
||||||
|
SetConsoleMode.errcheck = bool_errcheck
|
||||||
|
|
||||||
|
# As of Windows 10, the Windows console supports (some) ANSI escape
|
||||||
|
# sequences, but it needs to be enabled using `SetConsoleMode` first.
|
||||||
|
#
|
||||||
|
# More info on the escape sequences supported:
|
||||||
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
|
||||||
|
stderr = GetStdHandle(STD_ERROR_HANDLE)
|
||||||
|
flags = GetConsoleMode(stderr)
|
||||||
|
SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
enable_virtual_terminal_processing()
|
_enable()
|
||||||
except WindowsError:
|
except OSError:
|
||||||
pass
|
terminal_supports_color = False
|
||||||
|
else:
|
||||||
|
terminal_supports_color = True
|
||||||
|
else: # pragma: win32 no cover
|
||||||
|
terminal_supports_color = True
|
||||||
|
|
||||||
RED = '\033[41m'
|
RED = '\033[41m'
|
||||||
GREEN = '\033[42m'
|
GREEN = '\033[42m'
|
||||||
YELLOW = '\033[43;30m'
|
YELLOW = '\033[43;30m'
|
||||||
TURQUOISE = '\033[46;30m'
|
TURQUOISE = '\033[46;30m'
|
||||||
NORMAL = '\033[0m'
|
SUBTLE = '\033[2m'
|
||||||
|
NORMAL = '\033[m'
|
||||||
|
|
||||||
|
|
||||||
class InvalidColorSetting(ValueError):
|
def format_color(text: str, color: str, use_color_setting: bool) -> str:
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def format_color(text, color, use_color_setting):
|
|
||||||
"""Format text with color.
|
"""Format text with color.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -29,22 +72,38 @@ def format_color(text, color, use_color_setting):
|
||||||
color - The color start string
|
color - The color start string
|
||||||
use_color_setting - Whether or not to color
|
use_color_setting - Whether or not to color
|
||||||
"""
|
"""
|
||||||
if not use_color_setting:
|
if use_color_setting:
|
||||||
return text
|
return f'{color}{text}{NORMAL}'
|
||||||
else:
|
else:
|
||||||
return '{}{}{}'.format(color, text, NORMAL)
|
return text
|
||||||
|
|
||||||
|
|
||||||
COLOR_CHOICES = ('auto', 'always', 'never')
|
COLOR_CHOICES = ('auto', 'always', 'never')
|
||||||
|
|
||||||
|
|
||||||
def use_color(setting):
|
def use_color(setting: str) -> bool:
|
||||||
"""Choose whether to use color based on the command argument.
|
"""Choose whether to use color based on the command argument.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
setting - Either `auto`, `always`, or `never`
|
setting - Either `auto`, `always`, or `never`
|
||||||
"""
|
"""
|
||||||
if setting not in COLOR_CHOICES:
|
if setting not in COLOR_CHOICES:
|
||||||
raise InvalidColorSetting(setting)
|
raise ValueError(setting)
|
||||||
|
|
||||||
return setting == 'always' or (setting == 'auto' and sys.stdout.isatty())
|
return (
|
||||||
|
setting == 'always' or (
|
||||||
|
setting == 'auto' and
|
||||||
|
sys.stderr.isatty() and
|
||||||
|
terminal_supports_color and
|
||||||
|
os.getenv('TERM') != 'dumb'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_color_option(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
'--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
|
||||||
|
type=use_color,
|
||||||
|
metavar='{' + ','.join(COLOR_CHOICES) + '}',
|
||||||
|
help='Whether to use color in output. Defaults to `%(default)s`.',
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from ctypes import POINTER
|
|
||||||
from ctypes import windll
|
|
||||||
from ctypes import WinError
|
|
||||||
from ctypes import WINFUNCTYPE
|
|
||||||
from ctypes.wintypes import BOOL
|
|
||||||
from ctypes.wintypes import DWORD
|
|
||||||
from ctypes.wintypes import HANDLE
|
|
||||||
|
|
||||||
STD_OUTPUT_HANDLE = -11
|
|
||||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
|
|
||||||
|
|
||||||
|
|
||||||
def bool_errcheck(result, func, args):
|
|
||||||
if not result:
|
|
||||||
raise WinError()
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
|
|
||||||
("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),),
|
|
||||||
)
|
|
||||||
|
|
||||||
GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
|
|
||||||
("GetConsoleMode", windll.kernel32),
|
|
||||||
((1, "hConsoleHandle"), (2, "lpMode")),
|
|
||||||
)
|
|
||||||
GetConsoleMode.errcheck = bool_errcheck
|
|
||||||
|
|
||||||
SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
|
|
||||||
("SetConsoleMode", windll.kernel32),
|
|
||||||
((1, "hConsoleHandle"), (1, "dwMode")),
|
|
||||||
)
|
|
||||||
SetConsoleMode.errcheck = bool_errcheck
|
|
||||||
|
|
||||||
|
|
||||||
def enable_virtual_terminal_processing():
|
|
||||||
"""As of Windows 10, the Windows console supports (some) ANSI escape
|
|
||||||
sequences, but it needs to be enabled using `SetConsoleMode` first.
|
|
||||||
|
|
||||||
More info on the escape sequences supported:
|
|
||||||
https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
|
|
||||||
"""
|
|
||||||
stdout = GetStdHandle(STD_OUTPUT_HANDLE)
|
|
||||||
flags = GetConsoleMode(stdout)
|
|
||||||
SetConsoleMode(stdout, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
||||||
|
|
@ -1,150 +1,215 @@
|
||||||
from __future__ import print_function
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
import concurrent.futures
|
||||||
|
import os.path
|
||||||
import re
|
import re
|
||||||
from collections import OrderedDict
|
import tempfile
|
||||||
|
from collections.abc import Sequence
|
||||||
from aspy.yaml import ordered_dump
|
from typing import Any
|
||||||
from aspy.yaml import ordered_load
|
from typing import NamedTuple
|
||||||
from cfgv import remove_defaults
|
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import git
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
from pre_commit.clientlib import CONFIG_SCHEMA
|
from pre_commit import xargs
|
||||||
from pre_commit.clientlib import is_local_repo
|
from pre_commit.clientlib import InvalidManifestError
|
||||||
from pre_commit.clientlib import is_meta_repo
|
|
||||||
from pre_commit.clientlib import load_config
|
from pre_commit.clientlib import load_config
|
||||||
|
from pre_commit.clientlib import load_manifest
|
||||||
|
from pre_commit.clientlib import LOCAL
|
||||||
|
from pre_commit.clientlib import META
|
||||||
from pre_commit.commands.migrate_config import migrate_config
|
from pre_commit.commands.migrate_config import migrate_config
|
||||||
from pre_commit.repository import Repository
|
|
||||||
from pre_commit.util import CalledProcessError
|
from pre_commit.util import CalledProcessError
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
from pre_commit.yaml import yaml_dump
|
||||||
|
from pre_commit.yaml import yaml_load
|
||||||
|
|
||||||
|
|
||||||
|
class RevInfo(NamedTuple):
|
||||||
|
repo: str
|
||||||
|
rev: str
|
||||||
|
frozen: str | None = None
|
||||||
|
hook_ids: frozenset[str] = frozenset()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(cls, config: dict[str, Any]) -> RevInfo:
|
||||||
|
return cls(config['repo'], config['rev'])
|
||||||
|
|
||||||
|
def update(self, tags_only: bool, freeze: bool) -> RevInfo:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
_git = ('git', *git.NO_FS_MONITOR, '-C', tmp)
|
||||||
|
|
||||||
|
if tags_only:
|
||||||
|
tag_opt = '--abbrev=0'
|
||||||
|
else:
|
||||||
|
tag_opt = '--exact'
|
||||||
|
tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
|
||||||
|
|
||||||
|
git.init_repo(tmp, self.repo)
|
||||||
|
cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true')
|
||||||
|
cmd_output_b(
|
||||||
|
*_git, 'fetch', 'origin', 'HEAD',
|
||||||
|
'--quiet', '--filter=blob:none', '--tags',
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rev = cmd_output(*tag_cmd)[1].strip()
|
||||||
|
except CalledProcessError:
|
||||||
|
rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
|
||||||
|
else:
|
||||||
|
if tags_only:
|
||||||
|
rev = git.get_best_candidate_tag(rev, tmp)
|
||||||
|
|
||||||
|
frozen = None
|
||||||
|
if freeze:
|
||||||
|
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
|
||||||
|
if exact != rev:
|
||||||
|
rev, frozen = exact, rev
|
||||||
|
|
||||||
|
try:
|
||||||
|
# workaround for windows -- see #2865
|
||||||
|
cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}')
|
||||||
|
cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE)
|
||||||
|
except CalledProcessError:
|
||||||
|
pass # this will be caught by manifest validating code
|
||||||
|
try:
|
||||||
|
manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE))
|
||||||
|
except InvalidManifestError as e:
|
||||||
|
raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}')
|
||||||
|
else:
|
||||||
|
hook_ids = frozenset(hook['id'] for hook in manifest)
|
||||||
|
|
||||||
|
return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids)
|
||||||
|
|
||||||
|
|
||||||
class RepositoryCannotBeUpdatedError(RuntimeError):
|
class RepositoryCannotBeUpdatedError(RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _update_repo(repo_config, store, tags_only):
|
def _check_hooks_still_exist_at_rev(
|
||||||
"""Updates a repository to the tip of `master`. If the repository cannot
|
repo_config: dict[str, Any],
|
||||||
be updated because a hook that is configured does not exist in `master`,
|
info: RevInfo,
|
||||||
this raises a RepositoryCannotBeUpdatedError
|
) -> None:
|
||||||
|
|
||||||
Args:
|
|
||||||
repo_config - A config for a repository
|
|
||||||
"""
|
|
||||||
repo_path = store.clone(repo_config['repo'], repo_config['rev'])
|
|
||||||
|
|
||||||
cmd_output('git', 'fetch', cwd=repo_path)
|
|
||||||
tag_cmd = ('git', 'describe', 'origin/master', '--tags')
|
|
||||||
if tags_only:
|
|
||||||
tag_cmd += ('--abbrev=0',)
|
|
||||||
else:
|
|
||||||
tag_cmd += ('--exact',)
|
|
||||||
try:
|
|
||||||
rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
|
|
||||||
except CalledProcessError:
|
|
||||||
tag_cmd = ('git', 'rev-parse', 'origin/master')
|
|
||||||
rev = cmd_output(*tag_cmd, cwd=repo_path)[1].strip()
|
|
||||||
|
|
||||||
# Don't bother trying to update if our rev is the same
|
|
||||||
if rev == repo_config['rev']:
|
|
||||||
return repo_config
|
|
||||||
|
|
||||||
# Construct a new config with the head rev
|
|
||||||
new_config = OrderedDict(repo_config)
|
|
||||||
new_config['rev'] = rev
|
|
||||||
new_repo = Repository.create(new_config, store)
|
|
||||||
|
|
||||||
# See if any of our hooks were deleted with the new commits
|
# See if any of our hooks were deleted with the new commits
|
||||||
hooks = {hook['id'] for hook in repo_config['hooks']}
|
hooks = {hook['id'] for hook in repo_config['hooks']}
|
||||||
hooks_missing = hooks - (hooks & set(new_repo.manifest_hooks))
|
hooks_missing = hooks - info.hook_ids
|
||||||
if hooks_missing:
|
if hooks_missing:
|
||||||
raise RepositoryCannotBeUpdatedError(
|
raise RepositoryCannotBeUpdatedError(
|
||||||
'Cannot update because the tip of master is missing these hooks:\n'
|
f'[{info.repo}] Cannot update because the update target is '
|
||||||
'{}'.format(', '.join(sorted(hooks_missing))),
|
f'missing these hooks: {", ".join(sorted(hooks_missing))}',
|
||||||
)
|
)
|
||||||
|
|
||||||
return new_config
|
|
||||||
|
def _update_one(
|
||||||
|
i: int,
|
||||||
|
repo: dict[str, Any],
|
||||||
|
*,
|
||||||
|
tags_only: bool,
|
||||||
|
freeze: bool,
|
||||||
|
) -> tuple[int, RevInfo, RevInfo]:
|
||||||
|
old = RevInfo.from_config(repo)
|
||||||
|
new = old.update(tags_only=tags_only, freeze=freeze)
|
||||||
|
_check_hooks_still_exist_at_rev(repo, new)
|
||||||
|
return i, old, new
|
||||||
|
|
||||||
|
|
||||||
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([^\s#]+)(.*)$', re.DOTALL)
|
REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
|
||||||
REV_LINE_FMT = '{}rev:{}{}{}'
|
|
||||||
|
|
||||||
|
|
||||||
def _write_new_config_file(path, output):
|
def _original_lines(
|
||||||
original_contents = open(path).read()
|
path: str,
|
||||||
output = remove_defaults(output, CONFIG_SCHEMA)
|
rev_infos: list[RevInfo | None],
|
||||||
new_contents = ordered_dump(output, **C.YAML_DUMP_KWARGS)
|
retry: bool = False,
|
||||||
|
) -> tuple[list[str], list[int]]:
|
||||||
|
"""detect `rev:` lines or reformat the file"""
|
||||||
|
with open(path, newline='') as f:
|
||||||
|
original = f.read()
|
||||||
|
|
||||||
lines = original_contents.splitlines(True)
|
lines = original.splitlines(True)
|
||||||
rev_line_indices_reversed = list(reversed([
|
idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)]
|
||||||
i for i, line in enumerate(lines) if REV_LINE_RE.match(line)
|
if len(idxs) == len(rev_infos):
|
||||||
]))
|
return lines, idxs
|
||||||
|
elif retry:
|
||||||
for line in new_contents.splitlines(True):
|
raise AssertionError('could not find rev lines')
|
||||||
if REV_LINE_RE.match(line):
|
else:
|
||||||
# It's possible we didn't identify the rev lines in the original
|
with open(path, 'w') as f:
|
||||||
if not rev_line_indices_reversed:
|
f.write(yaml_dump(yaml_load(original)))
|
||||||
break
|
return _original_lines(path, rev_infos, retry=True)
|
||||||
line_index = rev_line_indices_reversed.pop()
|
|
||||||
original_line = lines[line_index]
|
|
||||||
orig_match = REV_LINE_RE.match(original_line)
|
|
||||||
new_match = REV_LINE_RE.match(line)
|
|
||||||
lines[line_index] = REV_LINE_FMT.format(
|
|
||||||
orig_match.group(1), orig_match.group(2),
|
|
||||||
new_match.group(3), orig_match.group(4),
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we failed to intelligently rewrite the rev lines, fall back to the
|
|
||||||
# pretty-formatted yaml output
|
|
||||||
to_write = ''.join(lines)
|
|
||||||
if remove_defaults(ordered_load(to_write), CONFIG_SCHEMA) != output:
|
|
||||||
to_write = new_contents
|
|
||||||
|
|
||||||
with open(path, 'w') as f:
|
|
||||||
f.write(to_write)
|
|
||||||
|
|
||||||
|
|
||||||
def autoupdate(runner, store, tags_only, repos=()):
|
def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
|
||||||
"""Auto-update the pre-commit config to the latest versions of repos."""
|
lines, idxs = _original_lines(path, rev_infos)
|
||||||
migrate_config(runner, quiet=True)
|
|
||||||
retv = 0
|
|
||||||
output_repos = []
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
input_config = load_config(runner.config_file_path)
|
for idx, rev_info in zip(idxs, rev_infos):
|
||||||
|
if rev_info is None:
|
||||||
for repo_config in input_config['repos']:
|
|
||||||
if (
|
|
||||||
is_local_repo(repo_config) or
|
|
||||||
is_meta_repo(repo_config) or
|
|
||||||
# Skip updating any repo_configs that aren't for the specified repo
|
|
||||||
repos and repo_config['repo'] not in repos
|
|
||||||
):
|
|
||||||
output_repos.append(repo_config)
|
|
||||||
continue
|
continue
|
||||||
output.write('Updating {}...'.format(repo_config['repo']))
|
match = REV_LINE_RE.match(lines[idx])
|
||||||
try:
|
assert match is not None
|
||||||
new_repo_config = _update_repo(repo_config, store, tags_only)
|
new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3])
|
||||||
except RepositoryCannotBeUpdatedError as error:
|
new_rev = new_rev_s.split(':', 1)[1].strip()
|
||||||
output.write_line(error.args[0])
|
if rev_info.frozen is not None:
|
||||||
output_repos.append(repo_config)
|
comment = f' # frozen: {rev_info.frozen}'
|
||||||
retv = 1
|
elif match[5].strip().startswith('# frozen:'):
|
||||||
continue
|
comment = ''
|
||||||
|
|
||||||
if new_repo_config['rev'] != repo_config['rev']:
|
|
||||||
changed = True
|
|
||||||
output.write_line('updating {} -> {}.'.format(
|
|
||||||
repo_config['rev'], new_repo_config['rev'],
|
|
||||||
))
|
|
||||||
output_repos.append(new_repo_config)
|
|
||||||
else:
|
else:
|
||||||
output.write_line('already up to date.')
|
comment = match[5]
|
||||||
output_repos.append(repo_config)
|
lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}'
|
||||||
|
|
||||||
|
with open(path, 'w', newline='') as f:
|
||||||
|
f.write(''.join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
def autoupdate(
|
||||||
|
config_file: str,
|
||||||
|
tags_only: bool,
|
||||||
|
freeze: bool,
|
||||||
|
repos: Sequence[str] = (),
|
||||||
|
jobs: int = 1,
|
||||||
|
) -> int:
|
||||||
|
"""Auto-update the pre-commit config to the latest versions of repos."""
|
||||||
|
migrate_config(config_file, quiet=True)
|
||||||
|
changed = False
|
||||||
|
retv = 0
|
||||||
|
|
||||||
|
config_repos = [
|
||||||
|
repo for repo in load_config(config_file)['repos']
|
||||||
|
if repo['repo'] not in {LOCAL, META}
|
||||||
|
]
|
||||||
|
|
||||||
|
rev_infos: list[RevInfo | None] = [None] * len(config_repos)
|
||||||
|
jobs = jobs or xargs.cpu_count() # 0 => number of cpus
|
||||||
|
jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread
|
||||||
|
jobs = max(jobs, 1) # at least one thread
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
|
||||||
|
futures = [
|
||||||
|
exe.submit(
|
||||||
|
_update_one,
|
||||||
|
i, repo, tags_only=tags_only, freeze=freeze,
|
||||||
|
)
|
||||||
|
for i, repo in enumerate(config_repos)
|
||||||
|
if not repos or repo['repo'] in repos
|
||||||
|
]
|
||||||
|
for future in concurrent.futures.as_completed(futures):
|
||||||
|
try:
|
||||||
|
i, old, new = future.result()
|
||||||
|
except RepositoryCannotBeUpdatedError as e:
|
||||||
|
output.write_line(str(e))
|
||||||
|
retv = 1
|
||||||
|
else:
|
||||||
|
if new.rev != old.rev:
|
||||||
|
changed = True
|
||||||
|
if new.frozen:
|
||||||
|
new_s = f'{new.frozen} (frozen)'
|
||||||
|
else:
|
||||||
|
new_s = new.rev
|
||||||
|
msg = f'updating {old.rev} -> {new_s}'
|
||||||
|
rev_infos[i] = new
|
||||||
|
else:
|
||||||
|
msg = 'already up to date!'
|
||||||
|
|
||||||
|
output.write_line(f'[{old.repo}] {msg}')
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
output_config = input_config.copy()
|
_write_new_config(config_file, rev_infos)
|
||||||
output_config['repos'] = output_repos
|
|
||||||
_write_new_config_file(runner.config_file_path, output_config)
|
|
||||||
|
|
||||||
return retv
|
return retv
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
from __future__ import print_function
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
|
from pre_commit.store import Store
|
||||||
from pre_commit.util import rmtree
|
from pre_commit.util import rmtree
|
||||||
|
|
||||||
|
|
||||||
def clean(store):
|
def clean(store: Store) -> int:
|
||||||
legacy_path = os.path.expanduser('~/.pre-commit')
|
legacy_path = os.path.expanduser('~/.pre-commit')
|
||||||
for directory in (store.directory, legacy_path):
|
for directory in (store.directory, legacy_path):
|
||||||
if os.path.exists(directory):
|
if os.path.exists(directory):
|
||||||
rmtree(directory)
|
rmtree(directory)
|
||||||
output.write_line('Cleaned {}.'.format(directory))
|
output.write_line(f'Cleaned {directory}.')
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
98
pre_commit/commands/gc.py
Normal file
98
pre_commit/commands/gc.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import output
|
||||||
|
from pre_commit.clientlib import InvalidConfigError
|
||||||
|
from pre_commit.clientlib import InvalidManifestError
|
||||||
|
from pre_commit.clientlib import load_config
|
||||||
|
from pre_commit.clientlib import load_manifest
|
||||||
|
from pre_commit.clientlib import LOCAL
|
||||||
|
from pre_commit.clientlib import META
|
||||||
|
from pre_commit.store import Store
|
||||||
|
from pre_commit.util import rmtree
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_used_repos(
|
||||||
|
store: Store,
|
||||||
|
all_repos: dict[tuple[str, str], str],
|
||||||
|
unused_repos: set[tuple[str, str]],
|
||||||
|
repo: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
if repo['repo'] == META:
|
||||||
|
return
|
||||||
|
elif repo['repo'] == LOCAL:
|
||||||
|
for hook in repo['hooks']:
|
||||||
|
deps = hook.get('additional_dependencies')
|
||||||
|
unused_repos.discard((
|
||||||
|
store.db_repo_name(repo['repo'], deps),
|
||||||
|
C.LOCAL_REPO_VERSION,
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
key = (repo['repo'], repo['rev'])
|
||||||
|
path = all_repos.get(key)
|
||||||
|
# can't inspect manifest if it isn't cloned
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
|
||||||
|
except InvalidManifestError:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
unused_repos.discard(key)
|
||||||
|
by_id = {hook['id']: hook for hook in manifest}
|
||||||
|
|
||||||
|
for hook in repo['hooks']:
|
||||||
|
if hook['id'] not in by_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
deps = hook.get(
|
||||||
|
'additional_dependencies',
|
||||||
|
by_id[hook['id']]['additional_dependencies'],
|
||||||
|
)
|
||||||
|
unused_repos.discard((
|
||||||
|
store.db_repo_name(repo['repo'], deps), repo['rev'],
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def _gc(store: Store) -> int:
|
||||||
|
with store.exclusive_lock(), store.connect() as db:
|
||||||
|
store._create_configs_table(db)
|
||||||
|
|
||||||
|
repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
|
||||||
|
all_repos = {(repo, ref): path for repo, ref, path in repos}
|
||||||
|
unused_repos = set(all_repos)
|
||||||
|
|
||||||
|
configs_rows = db.execute('SELECT path FROM configs').fetchall()
|
||||||
|
configs = [path for path, in configs_rows]
|
||||||
|
|
||||||
|
dead_configs = []
|
||||||
|
for config_path in configs:
|
||||||
|
try:
|
||||||
|
config = load_config(config_path)
|
||||||
|
except InvalidConfigError:
|
||||||
|
dead_configs.append(config_path)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for repo in config['repos']:
|
||||||
|
_mark_used_repos(store, all_repos, unused_repos, repo)
|
||||||
|
|
||||||
|
paths = [(path,) for path in dead_configs]
|
||||||
|
db.executemany('DELETE FROM configs WHERE path = ?', paths)
|
||||||
|
|
||||||
|
db.executemany(
|
||||||
|
'DELETE FROM repos WHERE repo = ? and ref = ?',
|
||||||
|
sorted(unused_repos),
|
||||||
|
)
|
||||||
|
for k in unused_repos:
|
||||||
|
rmtree(all_repos[k])
|
||||||
|
|
||||||
|
return len(unused_repos)
|
||||||
|
|
||||||
|
|
||||||
|
def gc(store: Store) -> int:
|
||||||
|
output.write_line(f'{_gc(store)} repo(s) removed.')
|
||||||
|
return 0
|
||||||
95
pre_commit/commands/hazmat.py
Normal file
95
pre_commit/commands/hazmat.py
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit.parse_shebang import normalize_cmd
|
||||||
|
|
||||||
|
|
||||||
|
def add_parsers(parser: argparse.ArgumentParser) -> None:
|
||||||
|
subparsers = parser.add_subparsers(dest='tool')
|
||||||
|
|
||||||
|
cd_parser = subparsers.add_parser(
|
||||||
|
'cd', help='cd to a subdir and run the command',
|
||||||
|
)
|
||||||
|
cd_parser.add_argument('subdir')
|
||||||
|
cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
ignore_exit_code_parser = subparsers.add_parser(
|
||||||
|
'ignore-exit-code', help='run the command but ignore the exit code',
|
||||||
|
)
|
||||||
|
ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
n1_parser = subparsers.add_parser(
|
||||||
|
'n1', help='run the command once per filename',
|
||||||
|
)
|
||||||
|
n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
|
||||||
|
tuple[str, ...],
|
||||||
|
tuple[str, ...],
|
||||||
|
]:
|
||||||
|
for idx, val in enumerate(reversed(cmd)):
|
||||||
|
if val == '--':
|
||||||
|
split = len(cmd) - idx
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise SystemExit('hazmat entry must end with `--`')
|
||||||
|
|
||||||
|
return cmd[:split - 1], cmd[split:]
|
||||||
|
|
||||||
|
|
||||||
|
def cd(subdir: str, cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd, filenames = _cmd_filenames(cmd)
|
||||||
|
|
||||||
|
prefix = f'{subdir}/'
|
||||||
|
new_filenames = []
|
||||||
|
for filename in filenames:
|
||||||
|
if not filename.startswith(prefix):
|
||||||
|
raise SystemExit(f'unexpected file without {prefix=}: {filename}')
|
||||||
|
else:
|
||||||
|
new_filenames.append(filename.removeprefix(prefix))
|
||||||
|
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
return subprocess.call((*cmd, *new_filenames), cwd=subdir)
|
||||||
|
|
||||||
|
|
||||||
|
def ignore_exit_code(cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
subprocess.call(cmd)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def n1(cmd: tuple[str, ...]) -> int:
|
||||||
|
cmd, filenames = _cmd_filenames(cmd)
|
||||||
|
cmd = normalize_cmd(cmd)
|
||||||
|
ret = 0
|
||||||
|
for filename in filenames:
|
||||||
|
ret |= subprocess.call((*cmd, filename))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def impl(args: argparse.Namespace) -> int:
|
||||||
|
args.cmd = tuple(args.cmd)
|
||||||
|
if args.tool == 'cd':
|
||||||
|
return cd(args.subdir, args.cmd)
|
||||||
|
elif args.tool == 'ignore-exit-code':
|
||||||
|
return ignore_exit_code(args.cmd)
|
||||||
|
elif args.tool == 'n1':
|
||||||
|
return n1(args.cmd)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f'unexpected tool: {args.tool}')
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
add_parsers(parser)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
return impl(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
272
pre_commit/commands/hook_impl.py
Normal file
272
pre_commit/commands/hook_impl.py
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os.path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit.commands.run import run
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.parse_shebang import normalize_cmd
|
||||||
|
from pre_commit.store import Store
|
||||||
|
|
||||||
|
Z40 = '0' * 40
|
||||||
|
|
||||||
|
|
||||||
|
def _run_legacy(
|
||||||
|
hook_type: str,
|
||||||
|
hook_dir: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
if os.environ.get('PRE_COMMIT_RUNNING_LEGACY'):
|
||||||
|
raise SystemExit(
|
||||||
|
f"bug: pre-commit's script is installed in migration mode\n"
|
||||||
|
f'run `pre-commit install -f --hook-type {hook_type}` to fix '
|
||||||
|
f'this\n\n'
|
||||||
|
f'Please report this bug at '
|
||||||
|
f'https://github.com/pre-commit/pre-commit/issues',
|
||||||
|
)
|
||||||
|
|
||||||
|
if hook_type == 'pre-push':
|
||||||
|
stdin = sys.stdin.buffer.read()
|
||||||
|
else:
|
||||||
|
stdin = b''
|
||||||
|
|
||||||
|
# not running in legacy mode
|
||||||
|
legacy_hook = os.path.join(hook_dir, f'{hook_type}.legacy')
|
||||||
|
if not os.access(legacy_hook, os.X_OK):
|
||||||
|
return 0, stdin
|
||||||
|
|
||||||
|
with envcontext((('PRE_COMMIT_RUNNING_LEGACY', '1'),)):
|
||||||
|
cmd = normalize_cmd((legacy_hook, *args))
|
||||||
|
return subprocess.run(cmd, input=stdin).returncode, stdin
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_config(
|
||||||
|
retv: int,
|
||||||
|
config: str,
|
||||||
|
skip_on_missing_config: bool,
|
||||||
|
) -> None:
|
||||||
|
if not os.path.isfile(config):
|
||||||
|
if skip_on_missing_config or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
|
||||||
|
print(f'`{config}` config file not found. Skipping `pre-commit`.')
|
||||||
|
raise SystemExit(retv)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f'No {config} file was found\n'
|
||||||
|
f'- To temporarily silence this, run '
|
||||||
|
f'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
|
||||||
|
f'- To permanently silence this, install pre-commit with the '
|
||||||
|
f'--allow-missing-config option\n'
|
||||||
|
f'- To uninstall pre-commit run `pre-commit uninstall`',
|
||||||
|
)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _ns(
|
||||||
|
hook_type: str,
|
||||||
|
color: bool,
|
||||||
|
*,
|
||||||
|
all_files: bool = False,
|
||||||
|
remote_branch: str | None = None,
|
||||||
|
local_branch: str | None = None,
|
||||||
|
from_ref: str | None = None,
|
||||||
|
to_ref: str | None = None,
|
||||||
|
pre_rebase_upstream: str | None = None,
|
||||||
|
pre_rebase_branch: str | None = None,
|
||||||
|
remote_name: str | None = None,
|
||||||
|
remote_url: str | None = None,
|
||||||
|
commit_msg_filename: str | None = None,
|
||||||
|
prepare_commit_message_source: str | None = None,
|
||||||
|
commit_object_name: str | None = None,
|
||||||
|
checkout_type: str | None = None,
|
||||||
|
is_squash_merge: str | None = None,
|
||||||
|
rewrite_command: str | None = None,
|
||||||
|
) -> argparse.Namespace:
|
||||||
|
return argparse.Namespace(
|
||||||
|
color=color,
|
||||||
|
hook_stage=hook_type,
|
||||||
|
remote_branch=remote_branch,
|
||||||
|
local_branch=local_branch,
|
||||||
|
from_ref=from_ref,
|
||||||
|
to_ref=to_ref,
|
||||||
|
pre_rebase_upstream=pre_rebase_upstream,
|
||||||
|
pre_rebase_branch=pre_rebase_branch,
|
||||||
|
remote_name=remote_name,
|
||||||
|
remote_url=remote_url,
|
||||||
|
commit_msg_filename=commit_msg_filename,
|
||||||
|
prepare_commit_message_source=prepare_commit_message_source,
|
||||||
|
commit_object_name=commit_object_name,
|
||||||
|
all_files=all_files,
|
||||||
|
checkout_type=checkout_type,
|
||||||
|
is_squash_merge=is_squash_merge,
|
||||||
|
rewrite_command=rewrite_command,
|
||||||
|
files=(),
|
||||||
|
hook=None,
|
||||||
|
verbose=False,
|
||||||
|
show_diff_on_failure=False,
|
||||||
|
fail_fast=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rev_exists(rev: str) -> bool:
|
||||||
|
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
|
||||||
|
|
||||||
|
|
||||||
|
def _pre_push_ns(
|
||||||
|
color: bool,
|
||||||
|
args: Sequence[str],
|
||||||
|
stdin: bytes,
|
||||||
|
) -> argparse.Namespace | None:
|
||||||
|
remote_name = args[0]
|
||||||
|
remote_url = args[1]
|
||||||
|
|
||||||
|
for line in stdin.decode().splitlines():
|
||||||
|
parts = line.rsplit(maxsplit=3)
|
||||||
|
local_branch, local_sha, remote_branch, remote_sha = parts
|
||||||
|
if local_sha == Z40:
|
||||||
|
continue
|
||||||
|
elif remote_sha != Z40 and _rev_exists(remote_sha):
|
||||||
|
return _ns(
|
||||||
|
'pre-push', color,
|
||||||
|
from_ref=remote_sha, to_ref=local_sha,
|
||||||
|
remote_branch=remote_branch,
|
||||||
|
local_branch=local_branch,
|
||||||
|
remote_name=remote_name, remote_url=remote_url,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# ancestors not found in remote
|
||||||
|
ancestors = subprocess.check_output((
|
||||||
|
'git', 'rev-list', local_sha, '--topo-order', '--reverse',
|
||||||
|
'--not', f'--remotes={remote_name}',
|
||||||
|
)).decode().strip()
|
||||||
|
if not ancestors:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
first_ancestor = ancestors.splitlines()[0]
|
||||||
|
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
|
||||||
|
roots = set(subprocess.check_output(cmd).decode().splitlines())
|
||||||
|
if first_ancestor in roots:
|
||||||
|
# pushing the whole tree including root commit
|
||||||
|
return _ns(
|
||||||
|
'pre-push', color,
|
||||||
|
all_files=True,
|
||||||
|
remote_name=remote_name, remote_url=remote_url,
|
||||||
|
remote_branch=remote_branch,
|
||||||
|
local_branch=local_branch,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^')
|
||||||
|
source = subprocess.check_output(rev_cmd).decode().strip()
|
||||||
|
return _ns(
|
||||||
|
'pre-push', color,
|
||||||
|
from_ref=source, to_ref=local_sha,
|
||||||
|
remote_name=remote_name, remote_url=remote_url,
|
||||||
|
remote_branch=remote_branch,
|
||||||
|
local_branch=local_branch,
|
||||||
|
)
|
||||||
|
|
||||||
|
# nothing to push
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_EXPECTED_ARG_LENGTH_BY_HOOK = {
|
||||||
|
'commit-msg': 1,
|
||||||
|
'post-checkout': 3,
|
||||||
|
'post-commit': 0,
|
||||||
|
'pre-commit': 0,
|
||||||
|
'pre-merge-commit': 0,
|
||||||
|
'post-merge': 1,
|
||||||
|
'post-rewrite': 1,
|
||||||
|
'pre-push': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
|
||||||
|
if hook_type == 'prepare-commit-msg':
|
||||||
|
if len(args) < 1 or len(args) > 3:
|
||||||
|
raise SystemExit(
|
||||||
|
f'hook-impl for {hook_type} expected 1, 2, or 3 arguments '
|
||||||
|
f'but got {len(args)}: {args}',
|
||||||
|
)
|
||||||
|
elif hook_type == 'pre-rebase':
|
||||||
|
if len(args) < 1 or len(args) > 2:
|
||||||
|
raise SystemExit(
|
||||||
|
f'hook-impl for {hook_type} expected 1 or 2 arguments '
|
||||||
|
f'but got {len(args)}: {args}',
|
||||||
|
)
|
||||||
|
elif hook_type in _EXPECTED_ARG_LENGTH_BY_HOOK:
|
||||||
|
expected = _EXPECTED_ARG_LENGTH_BY_HOOK[hook_type]
|
||||||
|
if len(args) != expected:
|
||||||
|
arguments_s = 'argument' if expected == 1 else 'arguments'
|
||||||
|
raise SystemExit(
|
||||||
|
f'hook-impl for {hook_type} expected {expected} {arguments_s} '
|
||||||
|
f'but got {len(args)}: {args}',
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AssertionError(f'unexpected hook type: {hook_type}')
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ns(
|
||||||
|
hook_type: str,
|
||||||
|
color: bool,
|
||||||
|
args: Sequence[str],
|
||||||
|
stdin: bytes,
|
||||||
|
) -> argparse.Namespace | None:
|
||||||
|
_check_args_length(hook_type, args)
|
||||||
|
if hook_type == 'pre-push':
|
||||||
|
return _pre_push_ns(color, args, stdin)
|
||||||
|
elif hook_type in 'commit-msg':
|
||||||
|
return _ns(hook_type, color, commit_msg_filename=args[0])
|
||||||
|
elif hook_type == 'prepare-commit-msg' and len(args) == 1:
|
||||||
|
return _ns(hook_type, color, commit_msg_filename=args[0])
|
||||||
|
elif hook_type == 'prepare-commit-msg' and len(args) == 2:
|
||||||
|
return _ns(
|
||||||
|
hook_type, color, commit_msg_filename=args[0],
|
||||||
|
prepare_commit_message_source=args[1],
|
||||||
|
)
|
||||||
|
elif hook_type == 'prepare-commit-msg' and len(args) == 3:
|
||||||
|
return _ns(
|
||||||
|
hook_type, color, commit_msg_filename=args[0],
|
||||||
|
prepare_commit_message_source=args[1], commit_object_name=args[2],
|
||||||
|
)
|
||||||
|
elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}:
|
||||||
|
return _ns(hook_type, color)
|
||||||
|
elif hook_type == 'post-checkout':
|
||||||
|
return _ns(
|
||||||
|
hook_type, color,
|
||||||
|
from_ref=args[0], to_ref=args[1], checkout_type=args[2],
|
||||||
|
)
|
||||||
|
elif hook_type == 'post-merge':
|
||||||
|
return _ns(hook_type, color, is_squash_merge=args[0])
|
||||||
|
elif hook_type == 'post-rewrite':
|
||||||
|
return _ns(hook_type, color, rewrite_command=args[0])
|
||||||
|
elif hook_type == 'pre-rebase' and len(args) == 1:
|
||||||
|
return _ns(hook_type, color, pre_rebase_upstream=args[0])
|
||||||
|
elif hook_type == 'pre-rebase' and len(args) == 2:
|
||||||
|
return _ns(
|
||||||
|
hook_type, color, pre_rebase_upstream=args[0],
|
||||||
|
pre_rebase_branch=args[1],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AssertionError(f'unexpected hook type: {hook_type}')
|
||||||
|
|
||||||
|
|
||||||
|
def hook_impl(
|
||||||
|
store: Store,
|
||||||
|
*,
|
||||||
|
config: str,
|
||||||
|
color: bool,
|
||||||
|
hook_type: str,
|
||||||
|
hook_dir: str,
|
||||||
|
skip_on_missing_config: bool,
|
||||||
|
args: Sequence[str],
|
||||||
|
) -> int:
|
||||||
|
retv, stdin = _run_legacy(hook_type, hook_dir, args)
|
||||||
|
_validate_config(retv, config, skip_on_missing_config)
|
||||||
|
ns = _run_ns(hook_type, color, args, stdin)
|
||||||
|
if ns is None:
|
||||||
|
return retv
|
||||||
|
else:
|
||||||
|
return retv | run(config, store, ns)
|
||||||
39
pre_commit/commands/init_templatedir.py
Normal file
39
pre_commit/commands/init_templatedir.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from pre_commit.commands.install_uninstall import install
|
||||||
|
from pre_commit.store import Store
|
||||||
|
from pre_commit.util import CalledProcessError
|
||||||
|
from pre_commit.util import cmd_output
|
||||||
|
|
||||||
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
|
|
||||||
|
def init_templatedir(
|
||||||
|
config_file: str,
|
||||||
|
store: Store,
|
||||||
|
directory: str,
|
||||||
|
hook_types: list[str] | None,
|
||||||
|
skip_on_missing_config: bool = True,
|
||||||
|
) -> int:
|
||||||
|
install(
|
||||||
|
config_file,
|
||||||
|
store,
|
||||||
|
hook_types=hook_types,
|
||||||
|
overwrite=True,
|
||||||
|
skip_on_missing_config=skip_on_missing_config,
|
||||||
|
git_dir=directory,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
_, out, _ = cmd_output('git', 'config', 'init.templateDir')
|
||||||
|
except CalledProcessError:
|
||||||
|
configured_path = None
|
||||||
|
else:
|
||||||
|
configured_path = os.path.realpath(os.path.expanduser(out.strip()))
|
||||||
|
dest = os.path.realpath(directory)
|
||||||
|
if configured_path != dest:
|
||||||
|
logger.warning('`init.templateDir` not set to the target directory')
|
||||||
|
logger.warning(f'maybe `git config --global init.templateDir {dest}`?')
|
||||||
|
return 0
|
||||||
|
|
@ -1,123 +1,167 @@
|
||||||
from __future__ import print_function
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import io
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
from pre_commit.repository import repositories
|
from pre_commit.clientlib import InvalidConfigError
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.clientlib import load_config
|
||||||
|
from pre_commit.repository import all_hooks
|
||||||
|
from pre_commit.repository import install_hook_envs
|
||||||
|
from pre_commit.store import Store
|
||||||
from pre_commit.util import make_executable
|
from pre_commit.util import make_executable
|
||||||
from pre_commit.util import mkdirp
|
from pre_commit.util import resource_text
|
||||||
from pre_commit.util import resource_filename
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# This is used to identify the hook file we install
|
# This is used to identify the hook file we install
|
||||||
PRIOR_HASHES = (
|
PRIOR_HASHES = (
|
||||||
'4d9958c90bc262f47553e2c073f14cfe',
|
b'4d9958c90bc262f47553e2c073f14cfe',
|
||||||
'd8ee923c46731b42cd95cc869add4062',
|
b'd8ee923c46731b42cd95cc869add4062',
|
||||||
'49fd668cb42069aa1b6048464be5d395',
|
b'49fd668cb42069aa1b6048464be5d395',
|
||||||
'79f09a650522a87b0da915d0d983b2de',
|
b'79f09a650522a87b0da915d0d983b2de',
|
||||||
'e358c9dae00eac5d06b38dfdb1e33a8c',
|
b'e358c9dae00eac5d06b38dfdb1e33a8c',
|
||||||
)
|
)
|
||||||
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03'
|
||||||
TEMPLATE_START = '# start templated\n'
|
TEMPLATE_START = '# start templated\n'
|
||||||
TEMPLATE_END = '# end templated\n'
|
TEMPLATE_END = '# end templated\n'
|
||||||
|
|
||||||
|
|
||||||
def _hook_paths(git_root, hook_type):
|
def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]:
|
||||||
pth = os.path.join(git.get_git_dir(git_root), 'hooks', hook_type)
|
if hook_types is not None:
|
||||||
return pth, '{}.legacy'.format(pth)
|
return hook_types
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
cfg = load_config(cfg_filename)
|
||||||
|
except InvalidConfigError:
|
||||||
|
return ['pre-commit']
|
||||||
|
else:
|
||||||
|
return cfg['default_install_hook_types']
|
||||||
|
|
||||||
|
|
||||||
def is_our_script(filename):
|
def _hook_paths(
|
||||||
if not os.path.exists(filename):
|
hook_type: str,
|
||||||
|
git_dir: str | None = None,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
git_dir = git_dir if git_dir is not None else git.get_git_common_dir()
|
||||||
|
pth = os.path.join(git_dir, 'hooks', hook_type)
|
||||||
|
return pth, f'{pth}.legacy'
|
||||||
|
|
||||||
|
|
||||||
|
def is_our_script(filename: str) -> bool:
|
||||||
|
if not os.path.exists(filename): # pragma: win32 no cover (symlink)
|
||||||
return False
|
return False
|
||||||
contents = io.open(filename).read()
|
with open(filename, 'rb') as f:
|
||||||
|
contents = f.read()
|
||||||
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
|
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
|
||||||
|
|
||||||
|
|
||||||
def install(
|
def _install_hook_script(
|
||||||
runner, store, overwrite=False, hooks=False, hook_type='pre-commit',
|
config_file: str,
|
||||||
skip_on_missing_conf=False,
|
hook_type: str,
|
||||||
):
|
overwrite: bool = False,
|
||||||
"""Install the pre-commit hooks."""
|
skip_on_missing_config: bool = False,
|
||||||
if cmd_output('git', 'config', 'core.hooksPath', retcode=None)[1].strip():
|
git_dir: str | None = None,
|
||||||
logger.error(
|
) -> None:
|
||||||
'Cowardly refusing to install hooks with `core.hooksPath` set.\n'
|
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
|
||||||
'hint: `git config --unset-all core.hooksPath`',
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
hook_path, legacy_path = _hook_paths(runner.git_root, hook_type)
|
os.makedirs(os.path.dirname(hook_path), exist_ok=True)
|
||||||
|
|
||||||
mkdirp(os.path.dirname(hook_path))
|
|
||||||
|
|
||||||
# If we have an existing hook, move it to pre-commit.legacy
|
# If we have an existing hook, move it to pre-commit.legacy
|
||||||
if os.path.lexists(hook_path) and not is_our_script(hook_path):
|
if os.path.lexists(hook_path) and not is_our_script(hook_path):
|
||||||
os.rename(hook_path, legacy_path)
|
shutil.move(hook_path, legacy_path)
|
||||||
|
|
||||||
# If we specify overwrite, we simply delete the legacy file
|
# If we specify overwrite, we simply delete the legacy file
|
||||||
if overwrite and os.path.exists(legacy_path):
|
if overwrite and os.path.exists(legacy_path):
|
||||||
os.remove(legacy_path)
|
os.remove(legacy_path)
|
||||||
elif os.path.exists(legacy_path):
|
elif os.path.exists(legacy_path):
|
||||||
output.write_line(
|
output.write_line(
|
||||||
'Running in migration mode with existing hooks at {}\n'
|
f'Running in migration mode with existing hooks at {legacy_path}\n'
|
||||||
'Use -f to use only pre-commit.'.format(legacy_path),
|
f'Use -f to use only pre-commit.',
|
||||||
)
|
)
|
||||||
|
|
||||||
params = {
|
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
|
||||||
'CONFIG': runner.config_file,
|
if skip_on_missing_config:
|
||||||
'HOOK_TYPE': hook_type,
|
args.append('--skip-on-missing-config')
|
||||||
'INSTALL_PYTHON': sys.executable,
|
|
||||||
'SKIP_ON_MISSING_CONFIG': skip_on_missing_conf,
|
|
||||||
}
|
|
||||||
|
|
||||||
with io.open(hook_path, 'w') as hook_file:
|
with open(hook_path, 'w') as hook_file:
|
||||||
with io.open(resource_filename('hook-tmpl')) as f:
|
contents = resource_text('hook-tmpl')
|
||||||
contents = f.read()
|
|
||||||
before, rest = contents.split(TEMPLATE_START)
|
before, rest = contents.split(TEMPLATE_START)
|
||||||
to_template, after = rest.split(TEMPLATE_END)
|
_, after = rest.split(TEMPLATE_END)
|
||||||
|
|
||||||
|
# on windows always use `/bin/sh` since `bash` might not be on PATH
|
||||||
|
# though we use bash-specific features `sh` on windows is actually
|
||||||
|
# bash in "POSIXLY_CORRECT" mode which still supports the features we
|
||||||
|
# use: subshells / arrays
|
||||||
|
if sys.platform == 'win32': # pragma: win32 cover
|
||||||
|
hook_file.write('#!/bin/sh\n')
|
||||||
|
|
||||||
hook_file.write(before + TEMPLATE_START)
|
hook_file.write(before + TEMPLATE_START)
|
||||||
for line in to_template.splitlines():
|
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
|
||||||
var = line.split()[0]
|
args_s = shlex.join(args)
|
||||||
hook_file.write('{} = {!r}\n'.format(var, params[var]))
|
hook_file.write(f'ARGS=({args_s})\n')
|
||||||
hook_file.write(TEMPLATE_END + after)
|
hook_file.write(TEMPLATE_END + after)
|
||||||
make_executable(hook_path)
|
make_executable(hook_path)
|
||||||
|
|
||||||
output.write_line('pre-commit installed at {}'.format(hook_path))
|
output.write_line(f'pre-commit installed at {hook_path}')
|
||||||
|
|
||||||
|
|
||||||
|
def install(
|
||||||
|
config_file: str,
|
||||||
|
store: Store,
|
||||||
|
hook_types: list[str] | None,
|
||||||
|
overwrite: bool = False,
|
||||||
|
hooks: bool = False,
|
||||||
|
skip_on_missing_config: bool = False,
|
||||||
|
git_dir: str | None = None,
|
||||||
|
) -> int:
|
||||||
|
if git_dir is None and git.has_core_hookpaths_set():
|
||||||
|
logger.error(
|
||||||
|
'Cowardly refusing to install hooks with `core.hooksPath` set.\n'
|
||||||
|
'hint: `git config --unset-all core.hooksPath`',
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for hook_type in _hook_types(config_file, hook_types):
|
||||||
|
_install_hook_script(
|
||||||
|
config_file, hook_type,
|
||||||
|
overwrite=overwrite,
|
||||||
|
skip_on_missing_config=skip_on_missing_config,
|
||||||
|
git_dir=git_dir,
|
||||||
|
)
|
||||||
|
|
||||||
# If they requested we install all of the hooks, do so.
|
|
||||||
if hooks:
|
if hooks:
|
||||||
install_hooks(runner, store)
|
install_hooks(config_file, store)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def install_hooks(runner, store):
|
def install_hooks(config_file: str, store: Store) -> int:
|
||||||
for repository in repositories(runner.config, store):
|
install_hook_envs(all_hooks(load_config(config_file), store), store)
|
||||||
repository.require_installed()
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def uninstall(runner, hook_type='pre-commit'):
|
def _uninstall_hook_script(hook_type: str) -> None:
|
||||||
"""Uninstall the pre-commit hooks."""
|
hook_path, legacy_path = _hook_paths(hook_type)
|
||||||
hook_path, legacy_path = _hook_paths(runner.git_root, hook_type)
|
|
||||||
|
|
||||||
# If our file doesn't exist or it isn't ours, gtfo.
|
# If our file doesn't exist or it isn't ours, gtfo.
|
||||||
if not os.path.exists(hook_path) or not is_our_script(hook_path):
|
if not os.path.exists(hook_path) or not is_our_script(hook_path):
|
||||||
return 0
|
return
|
||||||
|
|
||||||
os.remove(hook_path)
|
os.remove(hook_path)
|
||||||
output.write_line('{} uninstalled'.format(hook_type))
|
output.write_line(f'{hook_type} uninstalled')
|
||||||
|
|
||||||
if os.path.exists(legacy_path):
|
if os.path.exists(legacy_path):
|
||||||
os.rename(legacy_path, hook_path)
|
os.replace(legacy_path, hook_path)
|
||||||
output.write_line('Restored previous hooks to {}'.format(hook_path))
|
output.write_line(f'Restored previous hooks to {hook_path}')
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall(config_file: str, hook_types: list[str] | None) -> int:
|
||||||
|
for hook_type in _hook_types(config_file, hook_types):
|
||||||
|
_uninstall_hook_script(hook_type)
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,135 @@
|
||||||
from __future__ import print_function
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import io
|
import functools
|
||||||
import re
|
import itertools
|
||||||
|
import textwrap
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import cfgv
|
||||||
import yaml
|
import yaml
|
||||||
from aspy.yaml import ordered_load
|
from yaml.nodes import ScalarNode
|
||||||
|
|
||||||
|
from pre_commit.clientlib import InvalidConfigError
|
||||||
|
from pre_commit.yaml import yaml_compose
|
||||||
|
from pre_commit.yaml import yaml_load
|
||||||
|
from pre_commit.yaml_rewrite import MappingKey
|
||||||
|
from pre_commit.yaml_rewrite import MappingValue
|
||||||
|
from pre_commit.yaml_rewrite import match
|
||||||
|
from pre_commit.yaml_rewrite import SequenceItem
|
||||||
|
|
||||||
|
|
||||||
def _indent(s):
|
def _is_header_line(line: str) -> bool:
|
||||||
lines = s.splitlines(True)
|
return line.startswith(('#', '---')) or not line.strip()
|
||||||
return ''.join(' ' * 4 + line if line.strip() else line for line in lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_header_line(line):
|
def _migrate_map(contents: str) -> str:
|
||||||
return (line.startswith(('#', '---')) or not line.strip())
|
if isinstance(yaml_load(contents), list):
|
||||||
|
# Find the first non-header line
|
||||||
|
lines = contents.splitlines(True)
|
||||||
|
i = 0
|
||||||
|
# Only loop on non empty configuration file
|
||||||
|
while i < len(lines) and _is_header_line(lines[i]):
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
header = ''.join(lines[:i])
|
||||||
|
rest = ''.join(lines[i:])
|
||||||
|
|
||||||
def _migrate_map(contents):
|
|
||||||
# Find the first non-header line
|
|
||||||
lines = contents.splitlines(True)
|
|
||||||
i = 0
|
|
||||||
while _is_header_line(lines[i]):
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
header = ''.join(lines[:i])
|
|
||||||
rest = ''.join(lines[i:])
|
|
||||||
|
|
||||||
if isinstance(ordered_load(contents), list):
|
|
||||||
# If they are using the "default" flow style of yaml, this operation
|
# If they are using the "default" flow style of yaml, this operation
|
||||||
# will yield a valid configuration
|
# will yield a valid configuration
|
||||||
try:
|
try:
|
||||||
trial_contents = header + 'repos:\n' + rest
|
trial_contents = f'{header}repos:\n{rest}'
|
||||||
ordered_load(trial_contents)
|
yaml_load(trial_contents)
|
||||||
contents = trial_contents
|
contents = trial_contents
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
contents = header + 'repos:\n' + _indent(rest)
|
contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}'
|
||||||
|
|
||||||
return contents
|
return contents
|
||||||
|
|
||||||
|
|
||||||
def _migrate_sha_to_rev(contents):
|
def _preserve_style(n: ScalarNode, *, s: str) -> str:
|
||||||
reg = re.compile(r'(\n\s+)sha:')
|
style = n.style or ''
|
||||||
return reg.sub(r'\1rev:', contents)
|
return f'{style}{s}{style}'
|
||||||
|
|
||||||
|
|
||||||
def migrate_config(runner, quiet=False):
|
def _fix_stage(n: ScalarNode) -> str:
|
||||||
with io.open(runner.config_file_path) as f:
|
return _preserve_style(n, s=f'pre-{n.value}')
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_composed(contents: str) -> str:
|
||||||
|
tree = yaml_compose(contents)
|
||||||
|
rewrites: list[tuple[ScalarNode, Callable[[ScalarNode], str]]] = []
|
||||||
|
|
||||||
|
# sha -> rev
|
||||||
|
sha_to_rev_replace = functools.partial(_preserve_style, s='rev')
|
||||||
|
sha_to_rev_matcher = (
|
||||||
|
MappingValue('repos'),
|
||||||
|
SequenceItem(),
|
||||||
|
MappingKey('sha'),
|
||||||
|
)
|
||||||
|
for node in match(tree, sha_to_rev_matcher):
|
||||||
|
rewrites.append((node, sha_to_rev_replace))
|
||||||
|
|
||||||
|
# python_venv -> python
|
||||||
|
language_matcher = (
|
||||||
|
MappingValue('repos'),
|
||||||
|
SequenceItem(),
|
||||||
|
MappingValue('hooks'),
|
||||||
|
SequenceItem(),
|
||||||
|
MappingValue('language'),
|
||||||
|
)
|
||||||
|
python_venv_replace = functools.partial(_preserve_style, s='python')
|
||||||
|
for node in match(tree, language_matcher):
|
||||||
|
if node.value == 'python_venv':
|
||||||
|
rewrites.append((node, python_venv_replace))
|
||||||
|
|
||||||
|
# stages rewrites
|
||||||
|
default_stages_matcher = (MappingValue('default_stages'), SequenceItem())
|
||||||
|
default_stages_match = match(tree, default_stages_matcher)
|
||||||
|
hook_stages_matcher = (
|
||||||
|
MappingValue('repos'),
|
||||||
|
SequenceItem(),
|
||||||
|
MappingValue('hooks'),
|
||||||
|
SequenceItem(),
|
||||||
|
MappingValue('stages'),
|
||||||
|
SequenceItem(),
|
||||||
|
)
|
||||||
|
hook_stages_match = match(tree, hook_stages_matcher)
|
||||||
|
for node in itertools.chain(default_stages_match, hook_stages_match):
|
||||||
|
if node.value in {'commit', 'push', 'merge-commit'}:
|
||||||
|
rewrites.append((node, _fix_stage))
|
||||||
|
|
||||||
|
rewrites.sort(reverse=True, key=lambda nf: nf[0].start_mark.index)
|
||||||
|
|
||||||
|
src_parts = []
|
||||||
|
end: int | None = None
|
||||||
|
for node, func in rewrites:
|
||||||
|
src_parts.append(contents[node.end_mark.index:end])
|
||||||
|
src_parts.append(func(node))
|
||||||
|
end = node.start_mark.index
|
||||||
|
src_parts.append(contents[:end])
|
||||||
|
src_parts.reverse()
|
||||||
|
return ''.join(src_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_config(config_file: str, quiet: bool = False) -> int:
|
||||||
|
with open(config_file) as f:
|
||||||
orig_contents = contents = f.read()
|
orig_contents = contents = f.read()
|
||||||
|
|
||||||
|
with cfgv.reraise_as(InvalidConfigError):
|
||||||
|
with cfgv.validate_context(f'File {config_file}'):
|
||||||
|
try:
|
||||||
|
yaml_load(orig_contents)
|
||||||
|
except Exception as e:
|
||||||
|
raise cfgv.ValidationError(str(e))
|
||||||
|
|
||||||
contents = _migrate_map(contents)
|
contents = _migrate_map(contents)
|
||||||
contents = _migrate_sha_to_rev(contents)
|
contents = _migrate_composed(contents)
|
||||||
|
|
||||||
if contents != orig_contents:
|
if contents != orig_contents:
|
||||||
with io.open(runner.config_file_path, 'w') as f:
|
with open(config_file, 'w') as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
|
|
||||||
print('Configuration has been migrated.')
|
print('Configuration has been migrated.')
|
||||||
elif not quiet:
|
elif not quiet:
|
||||||
print('Configuration is already migrated.')
|
print('Configuration is already migrated.')
|
||||||
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -1,171 +1,248 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import time
|
||||||
|
import unicodedata
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from collections.abc import MutableMapping
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from identify.identify import tags_from_path
|
from identify.identify import tags_from_path
|
||||||
|
|
||||||
from pre_commit import color
|
from pre_commit import color
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
from pre_commit.output import get_hook_message
|
from pre_commit.all_languages import languages
|
||||||
from pre_commit.repository import repositories
|
from pre_commit.clientlib import load_config
|
||||||
|
from pre_commit.hook import Hook
|
||||||
|
from pre_commit.repository import all_hooks
|
||||||
|
from pre_commit.repository import install_hook_envs
|
||||||
from pre_commit.staged_files_only import staged_files_only
|
from pre_commit.staged_files_only import staged_files_only
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.store import Store
|
||||||
from pre_commit.util import memoize_by_cwd
|
from pre_commit.util import cmd_output_b
|
||||||
from pre_commit.util import noop_context
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('pre_commit')
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
|
|
||||||
tags_from_path = memoize_by_cwd(tags_from_path)
|
def _len_cjk(msg: str) -> int:
|
||||||
|
widths = {'A': 1, 'F': 2, 'H': 1, 'N': 1, 'Na': 1, 'W': 2}
|
||||||
|
return sum(widths[unicodedata.east_asian_width(c)] for c in msg)
|
||||||
|
|
||||||
|
|
||||||
def _get_skips(environ):
|
def _start_msg(*, start: str, cols: int, end_len: int) -> str:
|
||||||
skips = environ.get('SKIP', '')
|
dots = '.' * (cols - _len_cjk(start) - end_len - 1)
|
||||||
return {skip.strip() for skip in skips.split(',') if skip.strip()}
|
return f'{start}{dots}'
|
||||||
|
|
||||||
|
|
||||||
def _hook_msg_start(hook, verbose):
|
def _full_msg(
|
||||||
return '{}{}'.format(
|
*,
|
||||||
'[{}] '.format(hook['id']) if verbose else '', hook['name'],
|
start: str,
|
||||||
|
cols: int,
|
||||||
|
end_msg: str,
|
||||||
|
end_color: str,
|
||||||
|
use_color: bool,
|
||||||
|
postfix: str = '',
|
||||||
|
) -> str:
|
||||||
|
dots = '.' * (cols - _len_cjk(start) - len(postfix) - len(end_msg) - 1)
|
||||||
|
end = color.format_color(end_msg, end_color, use_color)
|
||||||
|
return f'{start}{dots}{postfix}{end}\n'
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_include_exclude(
|
||||||
|
names: Iterable[str],
|
||||||
|
include: str,
|
||||||
|
exclude: str,
|
||||||
|
) -> Generator[str]:
|
||||||
|
include_re, exclude_re = re.compile(include), re.compile(exclude)
|
||||||
|
return (
|
||||||
|
filename for filename in names
|
||||||
|
if include_re.search(filename)
|
||||||
|
if not exclude_re.search(filename)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _filter_by_include_exclude(filenames, include, exclude):
|
class Classifier:
|
||||||
include_re, exclude_re = re.compile(include), re.compile(exclude)
|
def __init__(self, filenames: Iterable[str]) -> None:
|
||||||
return [
|
self.filenames = [f for f in filenames if os.path.lexists(f)]
|
||||||
filename for filename in filenames
|
|
||||||
if (
|
@functools.cache
|
||||||
include_re.search(filename) and
|
def _types_for_file(self, filename: str) -> set[str]:
|
||||||
not exclude_re.search(filename) and
|
return tags_from_path(filename)
|
||||||
os.path.lexists(filename)
|
|
||||||
|
def by_types(
|
||||||
|
self,
|
||||||
|
names: Iterable[str],
|
||||||
|
types: Iterable[str],
|
||||||
|
types_or: Iterable[str],
|
||||||
|
exclude_types: Iterable[str],
|
||||||
|
) -> Generator[str]:
|
||||||
|
types = frozenset(types)
|
||||||
|
types_or = frozenset(types_or)
|
||||||
|
exclude_types = frozenset(exclude_types)
|
||||||
|
for filename in names:
|
||||||
|
tags = self._types_for_file(filename)
|
||||||
|
if (
|
||||||
|
tags >= types and
|
||||||
|
(not types_or or tags & types_or) and
|
||||||
|
not tags & exclude_types
|
||||||
|
):
|
||||||
|
yield filename
|
||||||
|
|
||||||
|
def filenames_for_hook(self, hook: Hook) -> Generator[str]:
|
||||||
|
return self.by_types(
|
||||||
|
filter_by_include_exclude(
|
||||||
|
self.filenames,
|
||||||
|
hook.files,
|
||||||
|
hook.exclude,
|
||||||
|
),
|
||||||
|
hook.types,
|
||||||
|
hook.types_or,
|
||||||
|
hook.exclude_types,
|
||||||
)
|
)
|
||||||
]
|
|
||||||
|
@classmethod
|
||||||
|
def from_config(
|
||||||
|
cls,
|
||||||
|
filenames: Iterable[str],
|
||||||
|
include: str,
|
||||||
|
exclude: str,
|
||||||
|
) -> Classifier:
|
||||||
|
# on windows we normalize all filenames to use forward slashes
|
||||||
|
# this makes it easier to filter using the `files:` regex
|
||||||
|
# this also makes improperly quoted shell-based hooks work better
|
||||||
|
# see #1173
|
||||||
|
if os.altsep == '/' and os.sep == '\\':
|
||||||
|
filenames = (f.replace(os.sep, os.altsep) for f in filenames)
|
||||||
|
filenames = filter_by_include_exclude(filenames, include, exclude)
|
||||||
|
return Classifier(filenames)
|
||||||
|
|
||||||
|
|
||||||
def _filter_by_types(filenames, types, exclude_types):
|
def _get_skips(environ: MutableMapping[str, str]) -> set[str]:
|
||||||
types, exclude_types = frozenset(types), frozenset(exclude_types)
|
skips = environ.get('SKIP', '')
|
||||||
ret = []
|
return {skip.strip() for skip in skips.split(',') if skip.strip()}
|
||||||
for filename in filenames:
|
|
||||||
tags = tags_from_path(filename)
|
|
||||||
if tags >= types and not tags & exclude_types:
|
|
||||||
ret.append(filename)
|
|
||||||
return tuple(ret)
|
|
||||||
|
|
||||||
|
|
||||||
SKIPPED = 'Skipped'
|
SKIPPED = 'Skipped'
|
||||||
NO_FILES = '(no files to check)'
|
NO_FILES = '(no files to check)'
|
||||||
|
|
||||||
|
|
||||||
def _run_single_hook(filenames, hook, repo, args, skips, cols):
|
def _subtle_line(s: str, use_color: bool) -> None:
|
||||||
include, exclude = hook['files'], hook['exclude']
|
output.write_line(color.format_color(s, color.SUBTLE, use_color))
|
||||||
filenames = _filter_by_include_exclude(filenames, include, exclude)
|
|
||||||
types, exclude_types = hook['types'], hook['exclude_types']
|
|
||||||
filenames = _filter_by_types(filenames, types, exclude_types)
|
|
||||||
|
|
||||||
if hook['language'] == 'pcre':
|
|
||||||
logger.warning(
|
def _run_single_hook(
|
||||||
'`{}` (from {}) uses the deprecated pcre language.\n'
|
classifier: Classifier,
|
||||||
'The pcre language is scheduled for removal in pre-commit 2.x.\n'
|
hook: Hook,
|
||||||
'The pygrep language is a more portable (and usually drop-in) '
|
skips: set[str],
|
||||||
'replacement.'.format(hook['id'], repo.repo_config['repo']),
|
cols: int,
|
||||||
|
diff_before: bytes,
|
||||||
|
verbose: bool,
|
||||||
|
use_color: bool,
|
||||||
|
) -> tuple[bool, bytes]:
|
||||||
|
filenames = tuple(classifier.filenames_for_hook(hook))
|
||||||
|
|
||||||
|
if hook.id in skips or hook.alias in skips:
|
||||||
|
output.write(
|
||||||
|
_full_msg(
|
||||||
|
start=hook.name,
|
||||||
|
end_msg=SKIPPED,
|
||||||
|
end_color=color.YELLOW,
|
||||||
|
use_color=use_color,
|
||||||
|
cols=cols,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
duration = None
|
||||||
if hook['id'] in skips:
|
|
||||||
output.write(get_hook_message(
|
|
||||||
_hook_msg_start(hook, args.verbose),
|
|
||||||
end_msg=SKIPPED,
|
|
||||||
end_color=color.YELLOW,
|
|
||||||
use_color=args.color,
|
|
||||||
cols=cols,
|
|
||||||
))
|
|
||||||
return 0
|
|
||||||
elif not filenames and not hook['always_run']:
|
|
||||||
output.write(get_hook_message(
|
|
||||||
_hook_msg_start(hook, args.verbose),
|
|
||||||
postfix=NO_FILES,
|
|
||||||
end_msg=SKIPPED,
|
|
||||||
end_color=color.TURQUOISE,
|
|
||||||
use_color=args.color,
|
|
||||||
cols=cols,
|
|
||||||
))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Print the hook and the dots first in case the hook takes hella long to
|
|
||||||
# run.
|
|
||||||
output.write(get_hook_message(
|
|
||||||
_hook_msg_start(hook, args.verbose), end_len=6, cols=cols,
|
|
||||||
))
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
diff_before = cmd_output(
|
|
||||||
'git', 'diff', '--no-ext-diff', retcode=None, encoding=None,
|
|
||||||
)
|
|
||||||
retcode, stdout, stderr = repo.run_hook(
|
|
||||||
hook, tuple(filenames) if hook['pass_filenames'] else (),
|
|
||||||
)
|
|
||||||
diff_after = cmd_output(
|
|
||||||
'git', 'diff', '--no-ext-diff', retcode=None, encoding=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
file_modifications = diff_before != diff_after
|
|
||||||
|
|
||||||
# If the hook makes changes, fail the commit
|
|
||||||
if file_modifications:
|
|
||||||
retcode = 1
|
|
||||||
|
|
||||||
if retcode:
|
|
||||||
retcode = 1
|
|
||||||
print_color = color.RED
|
|
||||||
pass_fail = 'Failed'
|
|
||||||
else:
|
|
||||||
retcode = 0
|
retcode = 0
|
||||||
print_color = color.GREEN
|
diff_after = diff_before
|
||||||
pass_fail = 'Passed'
|
files_modified = False
|
||||||
|
out = b''
|
||||||
|
elif not filenames and not hook.always_run:
|
||||||
|
output.write(
|
||||||
|
_full_msg(
|
||||||
|
start=hook.name,
|
||||||
|
postfix=NO_FILES,
|
||||||
|
end_msg=SKIPPED,
|
||||||
|
end_color=color.TURQUOISE,
|
||||||
|
use_color=use_color,
|
||||||
|
cols=cols,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
duration = None
|
||||||
|
retcode = 0
|
||||||
|
diff_after = diff_before
|
||||||
|
files_modified = False
|
||||||
|
out = b''
|
||||||
|
else:
|
||||||
|
# print hook and dots first in case the hook takes a while to run
|
||||||
|
output.write(_start_msg(start=hook.name, end_len=6, cols=cols))
|
||||||
|
|
||||||
output.write_line(color.format_color(pass_fail, print_color, args.color))
|
if not hook.pass_filenames:
|
||||||
|
filenames = ()
|
||||||
|
time_before = time.monotonic()
|
||||||
|
language = languages[hook.language]
|
||||||
|
with language.in_env(hook.prefix, hook.language_version):
|
||||||
|
retcode, out = language.run_hook(
|
||||||
|
hook.prefix,
|
||||||
|
hook.entry,
|
||||||
|
hook.args,
|
||||||
|
filenames,
|
||||||
|
is_local=hook.src == 'local',
|
||||||
|
require_serial=hook.require_serial,
|
||||||
|
color=use_color,
|
||||||
|
)
|
||||||
|
duration = round(time.monotonic() - time_before, 2) or 0
|
||||||
|
diff_after = _get_diff()
|
||||||
|
|
||||||
if (
|
# if the hook makes changes, fail the commit
|
||||||
(stdout or stderr or file_modifications) and
|
files_modified = diff_before != diff_after
|
||||||
(retcode or args.verbose or hook['verbose'])
|
|
||||||
):
|
if retcode or files_modified:
|
||||||
output.write_line('hookid: {}\n'.format(hook['id']))
|
print_color = color.RED
|
||||||
|
status = 'Failed'
|
||||||
|
else:
|
||||||
|
print_color = color.GREEN
|
||||||
|
status = 'Passed'
|
||||||
|
|
||||||
|
output.write_line(color.format_color(status, print_color, use_color))
|
||||||
|
|
||||||
|
if verbose or hook.verbose or retcode or files_modified:
|
||||||
|
_subtle_line(f'- hook id: {hook.id}', use_color)
|
||||||
|
|
||||||
|
if (verbose or hook.verbose) and duration is not None:
|
||||||
|
_subtle_line(f'- duration: {duration}s', use_color)
|
||||||
|
|
||||||
|
if retcode:
|
||||||
|
_subtle_line(f'- exit code: {retcode}', use_color)
|
||||||
|
|
||||||
# Print a message if failing due to file modifications
|
# Print a message if failing due to file modifications
|
||||||
if file_modifications:
|
if files_modified:
|
||||||
output.write('Files were modified by this hook.')
|
_subtle_line('- files were modified by this hook', use_color)
|
||||||
|
|
||||||
if stdout or stderr:
|
|
||||||
output.write_line(' Additional output:')
|
|
||||||
|
|
||||||
|
if out.strip():
|
||||||
|
output.write_line()
|
||||||
|
output.write_line_b(out.strip(), logfile_name=hook.log_file)
|
||||||
output.write_line()
|
output.write_line()
|
||||||
|
|
||||||
for out in (stdout, stderr):
|
return files_modified or bool(retcode), diff_after
|
||||||
assert type(out) is bytes, type(out)
|
|
||||||
if out.strip():
|
|
||||||
output.write_line(out.strip(), logfile_name=hook['log_file'])
|
|
||||||
output.write_line()
|
|
||||||
|
|
||||||
return retcode
|
|
||||||
|
|
||||||
|
|
||||||
def _compute_cols(hooks, verbose):
|
def _compute_cols(hooks: Sequence[Hook]) -> int:
|
||||||
"""Compute the number of columns to display hook messages. The widest
|
"""Compute the number of columns to display hook messages. The widest
|
||||||
that will be displayed is in the no files skipped case:
|
that will be displayed is in the no files skipped case:
|
||||||
|
|
||||||
Hook name...(no files to check) Skipped
|
Hook name...(no files to check) Skipped
|
||||||
|
|
||||||
or in the verbose case
|
|
||||||
|
|
||||||
Hook name [hookid]...(no files to check) Skipped
|
|
||||||
"""
|
"""
|
||||||
if hooks:
|
if hooks:
|
||||||
name_len = max(len(_hook_msg_start(hook, verbose)) for hook in hooks)
|
name_len = max(_len_cjk(hook.name) for hook in hooks)
|
||||||
else:
|
else:
|
||||||
name_len = 0
|
name_len = 0
|
||||||
|
|
||||||
|
|
@ -173,11 +250,17 @@ def _compute_cols(hooks, verbose):
|
||||||
return max(cols, 80)
|
return max(cols, 80)
|
||||||
|
|
||||||
|
|
||||||
def _all_filenames(args):
|
def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
|
||||||
if args.origin and args.source:
|
# these hooks do not operate on files
|
||||||
return git.get_changed_files(args.origin, args.source)
|
if args.hook_stage in {
|
||||||
elif args.hook_stage == 'commit-msg':
|
'post-checkout', 'post-commit', 'post-merge', 'post-rewrite',
|
||||||
|
'pre-rebase',
|
||||||
|
}:
|
||||||
|
return ()
|
||||||
|
elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
|
||||||
return (args.commit_msg_filename,)
|
return (args.commit_msg_filename,)
|
||||||
|
elif args.from_ref and args.to_ref:
|
||||||
|
return git.get_changed_files(args.from_ref, args.to_ref)
|
||||||
elif args.files:
|
elif args.files:
|
||||||
return args.files
|
return args.files
|
||||||
elif args.all_files:
|
elif args.all_files:
|
||||||
|
|
@ -188,83 +271,178 @@ def _all_filenames(args):
|
||||||
return git.get_staged_files()
|
return git.get_staged_files()
|
||||||
|
|
||||||
|
|
||||||
def _run_hooks(config, repo_hooks, args, environ):
|
def _get_diff() -> bytes:
|
||||||
|
_, out, _ = cmd_output_b(
|
||||||
|
'git', 'diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules',
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _run_hooks(
|
||||||
|
config: dict[str, Any],
|
||||||
|
hooks: Sequence[Hook],
|
||||||
|
skips: set[str],
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> int:
|
||||||
"""Actually run the hooks."""
|
"""Actually run the hooks."""
|
||||||
skips = _get_skips(environ)
|
cols = _compute_cols(hooks)
|
||||||
cols = _compute_cols([hook for _, hook in repo_hooks], args.verbose)
|
classifier = Classifier.from_config(
|
||||||
filenames = _all_filenames(args)
|
_all_filenames(args), config['files'], config['exclude'],
|
||||||
filenames = _filter_by_include_exclude(filenames, '', config['exclude'])
|
)
|
||||||
retval = 0
|
retval = 0
|
||||||
for repo, hook in repo_hooks:
|
prior_diff = _get_diff()
|
||||||
retval |= _run_single_hook(filenames, hook, repo, args, skips, cols)
|
for hook in hooks:
|
||||||
if retval and config['fail_fast']:
|
current_retval, prior_diff = _run_single_hook(
|
||||||
|
classifier, hook, skips, cols, prior_diff,
|
||||||
|
verbose=args.verbose, use_color=args.color,
|
||||||
|
)
|
||||||
|
retval |= current_retval
|
||||||
|
fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast)
|
||||||
|
if current_retval and fail_fast:
|
||||||
break
|
break
|
||||||
if (
|
if retval and args.show_diff_on_failure and prior_diff:
|
||||||
retval and
|
if args.all_files:
|
||||||
args.show_diff_on_failure and
|
output.write_line(
|
||||||
subprocess.call(('git', 'diff', '--quiet', '--no-ext-diff')) != 0
|
'pre-commit hook(s) made changes.\n'
|
||||||
):
|
'If you are seeing this message in CI, '
|
||||||
|
'reproduce locally with: `pre-commit run --all-files`.\n'
|
||||||
|
'To run `pre-commit` as part of git workflow, use '
|
||||||
|
'`pre-commit install`.',
|
||||||
|
)
|
||||||
output.write_line('All changes made by hooks:')
|
output.write_line('All changes made by hooks:')
|
||||||
subprocess.call(('git', '--no-pager', 'diff', '--no-ext-diff'))
|
# args.color is a boolean.
|
||||||
|
# See user_color function in color.py
|
||||||
|
git_color_opt = 'always' if args.color else 'never'
|
||||||
|
subprocess.call((
|
||||||
|
'git', '--no-pager', 'diff', '--no-ext-diff',
|
||||||
|
f'--color={git_color_opt}',
|
||||||
|
))
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def _has_unmerged_paths():
|
def _has_unmerged_paths() -> bool:
|
||||||
_, stdout, _ = cmd_output('git', 'ls-files', '--unmerged')
|
_, stdout, _ = cmd_output_b('git', 'ls-files', '--unmerged')
|
||||||
return bool(stdout.strip())
|
return bool(stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
def _has_unstaged_config(runner):
|
def _has_unstaged_config(config_file: str) -> bool:
|
||||||
retcode, _, _ = cmd_output(
|
retcode, _, _ = cmd_output_b(
|
||||||
'git', 'diff', '--no-ext-diff', '--exit-code', runner.config_file_path,
|
'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False,
|
||||||
retcode=None,
|
|
||||||
)
|
)
|
||||||
# be explicit, other git errors don't mean it has an unstaged config.
|
# be explicit, other git errors don't mean it has an unstaged config.
|
||||||
return retcode == 1
|
return retcode == 1
|
||||||
|
|
||||||
|
|
||||||
def run(runner, store, args, environ=os.environ):
|
def run(
|
||||||
no_stash = args.all_files or bool(args.files)
|
config_file: str,
|
||||||
|
store: Store,
|
||||||
|
args: argparse.Namespace,
|
||||||
|
environ: MutableMapping[str, str] = os.environ,
|
||||||
|
) -> int:
|
||||||
|
stash = not args.all_files and not args.files
|
||||||
|
|
||||||
# Check if we have unresolved merge conflict files and fail fast.
|
# Check if we have unresolved merge conflict files and fail fast.
|
||||||
if _has_unmerged_paths():
|
if stash and _has_unmerged_paths():
|
||||||
logger.error('Unmerged files. Resolve before committing.')
|
logger.error('Unmerged files. Resolve before committing.')
|
||||||
return 1
|
return 1
|
||||||
if bool(args.source) != bool(args.origin):
|
if bool(args.from_ref) != bool(args.to_ref):
|
||||||
logger.error('Specify both --origin and --source.')
|
logger.error('Specify both --from-ref and --to-ref.')
|
||||||
return 1
|
return 1
|
||||||
if _has_unstaged_config(runner) and not no_stash:
|
if stash and _has_unstaged_config(config_file):
|
||||||
logger.error(
|
logger.error(
|
||||||
'Your pre-commit configuration is unstaged.\n'
|
f'Your pre-commit configuration is unstaged.\n'
|
||||||
'`git add {}` to fix this.'.format(runner.config_file),
|
f'`git add {config_file}` to fix this.',
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
if (
|
||||||
|
args.hook_stage in {'prepare-commit-msg', 'commit-msg'} and
|
||||||
|
not args.commit_msg_filename
|
||||||
|
):
|
||||||
|
logger.error(
|
||||||
|
f'`--commit-msg-filename` is required for '
|
||||||
|
f'`--hook-stage {args.hook_stage}`',
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
# prevent recursive post-checkout hooks (#1418)
|
||||||
|
if (
|
||||||
|
args.hook_stage == 'post-checkout' and
|
||||||
|
environ.get('_PRE_COMMIT_SKIP_POST_CHECKOUT')
|
||||||
|
):
|
||||||
|
return 0
|
||||||
|
|
||||||
# Expose origin / source as environment variables for hooks to consume
|
# Expose prepare_commit_message_source / commit_object_name
|
||||||
if args.origin and args.source:
|
# as environment variables for the hooks
|
||||||
environ['PRE_COMMIT_ORIGIN'] = args.origin
|
if args.prepare_commit_message_source:
|
||||||
environ['PRE_COMMIT_SOURCE'] = args.source
|
environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = (
|
||||||
|
args.prepare_commit_message_source
|
||||||
|
)
|
||||||
|
|
||||||
if no_stash:
|
if args.commit_object_name:
|
||||||
ctx = noop_context()
|
environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name
|
||||||
else:
|
|
||||||
ctx = staged_files_only(store.directory)
|
|
||||||
|
|
||||||
with ctx:
|
# Expose from-ref / to-ref as environment variables for hooks to consume
|
||||||
repo_hooks = []
|
if args.from_ref and args.to_ref:
|
||||||
for repo in repositories(runner.config, store):
|
# legacy names
|
||||||
for _, hook in repo.hooks:
|
environ['PRE_COMMIT_ORIGIN'] = args.from_ref
|
||||||
if (
|
environ['PRE_COMMIT_SOURCE'] = args.to_ref
|
||||||
(not args.hook or hook['id'] == args.hook) and
|
# new names
|
||||||
(not hook['stages'] or args.hook_stage in hook['stages'])
|
environ['PRE_COMMIT_FROM_REF'] = args.from_ref
|
||||||
):
|
environ['PRE_COMMIT_TO_REF'] = args.to_ref
|
||||||
repo_hooks.append((repo, hook))
|
|
||||||
|
|
||||||
if args.hook and not repo_hooks:
|
if args.pre_rebase_upstream and args.pre_rebase_branch:
|
||||||
output.write_line('No hook with id `{}`'.format(args.hook))
|
environ['PRE_COMMIT_PRE_REBASE_UPSTREAM'] = args.pre_rebase_upstream
|
||||||
|
environ['PRE_COMMIT_PRE_REBASE_BRANCH'] = args.pre_rebase_branch
|
||||||
|
|
||||||
|
if (
|
||||||
|
args.remote_name and args.remote_url and
|
||||||
|
args.remote_branch and args.local_branch
|
||||||
|
):
|
||||||
|
environ['PRE_COMMIT_LOCAL_BRANCH'] = args.local_branch
|
||||||
|
environ['PRE_COMMIT_REMOTE_BRANCH'] = args.remote_branch
|
||||||
|
environ['PRE_COMMIT_REMOTE_NAME'] = args.remote_name
|
||||||
|
environ['PRE_COMMIT_REMOTE_URL'] = args.remote_url
|
||||||
|
|
||||||
|
if args.checkout_type:
|
||||||
|
environ['PRE_COMMIT_CHECKOUT_TYPE'] = args.checkout_type
|
||||||
|
|
||||||
|
if args.is_squash_merge:
|
||||||
|
environ['PRE_COMMIT_IS_SQUASH_MERGE'] = args.is_squash_merge
|
||||||
|
|
||||||
|
if args.rewrite_command:
|
||||||
|
environ['PRE_COMMIT_REWRITE_COMMAND'] = args.rewrite_command
|
||||||
|
|
||||||
|
# Set pre_commit flag
|
||||||
|
environ['PRE_COMMIT'] = '1'
|
||||||
|
|
||||||
|
with contextlib.ExitStack() as exit_stack:
|
||||||
|
if stash:
|
||||||
|
exit_stack.enter_context(staged_files_only(store.directory))
|
||||||
|
|
||||||
|
config = load_config(config_file)
|
||||||
|
hooks = [
|
||||||
|
hook
|
||||||
|
for hook in all_hooks(config, store)
|
||||||
|
if not args.hook or hook.id == args.hook or hook.alias == args.hook
|
||||||
|
if args.hook_stage in hook.stages
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.hook and not hooks:
|
||||||
|
output.write_line(
|
||||||
|
f'No hook with id `{args.hook}` in stage `{args.hook_stage}`',
|
||||||
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
for repo in {repo for repo, _ in repo_hooks}:
|
skips = _get_skips(environ)
|
||||||
repo.require_installed()
|
to_install = [
|
||||||
|
hook
|
||||||
|
for hook in hooks
|
||||||
|
if hook.id not in skips and hook.alias not in skips
|
||||||
|
]
|
||||||
|
install_hook_envs(to_install, store)
|
||||||
|
|
||||||
return _run_hooks(runner.config, repo_hooks, args, environ)
|
return _run_hooks(config, hooks, skips, args)
|
||||||
|
|
||||||
|
# https://github.com/python/mypy/issues/7726
|
||||||
|
raise AssertionError('unreachable')
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: maybe `git ls-remote git://github.com/pre-commit/pre-commit-hooks` to
|
|
||||||
# determine the latest revision? This adds ~200ms from my tests (and is
|
|
||||||
# significantly faster than https:// or http://). For now, periodically
|
|
||||||
# manually updating the revision is fine.
|
|
||||||
SAMPLE_CONFIG = '''\
|
SAMPLE_CONFIG = '''\
|
||||||
# See https://pre-commit.com for more information
|
# See https://pre-commit.com for more information
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v1.2.1-1
|
rev: v3.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
|
|
@ -21,6 +13,6 @@ repos:
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def sample_config():
|
def sample_config() -> int:
|
||||||
print(SAMPLE_CONFIG, end='')
|
print(SAMPLE_CONFIG, end='')
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,68 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import collections
|
import argparse
|
||||||
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
import tempfile
|
||||||
from aspy.yaml import ordered_dump
|
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
from pre_commit.clientlib import load_manifest
|
from pre_commit.clientlib import load_manifest
|
||||||
from pre_commit.commands.run import run
|
from pre_commit.commands.run import run
|
||||||
from pre_commit.runner import Runner
|
|
||||||
from pre_commit.store import Store
|
from pre_commit.store import Store
|
||||||
from pre_commit.util import tmpdir
|
from pre_commit.util import cmd_output_b
|
||||||
|
from pre_commit.xargs import xargs
|
||||||
|
from pre_commit.yaml import yaml_dump
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def try_repo(args):
|
def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]:
|
||||||
ref = args.ref or git.head_rev(args.repo)
|
# if `ref` is explicitly passed, use it
|
||||||
|
if ref is not None:
|
||||||
|
return repo, ref
|
||||||
|
|
||||||
|
ref = git.head_rev(repo)
|
||||||
|
# if it exists on disk, we'll try and clone it with the local changes
|
||||||
|
if os.path.exists(repo) and git.has_diff('HEAD', repo=repo):
|
||||||
|
logger.warning('Creating temporary repo with uncommitted changes...')
|
||||||
|
|
||||||
|
shadow = os.path.join(tmpdir, 'shadow-repo')
|
||||||
|
cmd_output_b('git', 'clone', repo, shadow)
|
||||||
|
cmd_output_b('git', 'checkout', ref, '-b', '_pc_tmp', cwd=shadow)
|
||||||
|
|
||||||
|
idx = git.git_path('index', repo=shadow)
|
||||||
|
objs = git.git_path('objects', repo=shadow)
|
||||||
|
env = dict(os.environ, GIT_INDEX_FILE=idx, GIT_OBJECT_DIRECTORY=objs)
|
||||||
|
|
||||||
|
staged_files = git.get_staged_files(cwd=repo)
|
||||||
|
if staged_files:
|
||||||
|
xargs(('git', 'add', '--'), staged_files, cwd=repo, env=env)
|
||||||
|
|
||||||
|
cmd_output_b('git', 'add', '-u', cwd=repo, env=env)
|
||||||
|
git.commit(repo=shadow)
|
||||||
|
|
||||||
|
return shadow, git.head_rev(shadow)
|
||||||
|
else:
|
||||||
|
return repo, ref
|
||||||
|
|
||||||
|
|
||||||
|
def try_repo(args: argparse.Namespace) -> int:
|
||||||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
|
repo, ref = _repo_ref(tempdir, args.repo, args.ref)
|
||||||
|
|
||||||
with tmpdir() as tempdir:
|
|
||||||
store = Store(tempdir)
|
store = Store(tempdir)
|
||||||
if args.hook:
|
if args.hook:
|
||||||
hooks = [{'id': args.hook}]
|
hooks = [{'id': args.hook}]
|
||||||
else:
|
else:
|
||||||
repo_path = store.clone(args.repo, ref)
|
repo_path = store.clone(repo, ref)
|
||||||
manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE))
|
manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE))
|
||||||
manifest = sorted(manifest, key=lambda hook: hook['id'])
|
manifest = sorted(manifest, key=lambda hook: hook['id'])
|
||||||
hooks = [{'id': hook['id']} for hook in manifest]
|
hooks = [{'id': hook['id']} for hook in manifest]
|
||||||
|
|
||||||
items = (('repo', args.repo), ('rev', ref), ('hooks', hooks))
|
config = {'repos': [{'repo': repo, 'rev': ref, 'hooks': hooks}]}
|
||||||
config = {'repos': [collections.OrderedDict(items)]}
|
config_s = yaml_dump(config)
|
||||||
config_s = ordered_dump(config, **C.YAML_DUMP_KWARGS)
|
|
||||||
|
|
||||||
config_filename = os.path.join(tempdir, C.CONFIG_FILE)
|
config_filename = os.path.join(tempdir, C.CONFIG_FILE)
|
||||||
with open(config_filename, 'w') as cfg:
|
with open(config_filename, 'w') as cfg:
|
||||||
|
|
@ -43,4 +74,4 @@ def try_repo(args):
|
||||||
output.write(config_s)
|
output.write(config_s)
|
||||||
output.write_line('=' * 79)
|
output.write_line('=' * 79)
|
||||||
|
|
||||||
return run(Runner('.', config_filename), store, args)
|
return run(config_filename, store, args)
|
||||||
|
|
|
||||||
18
pre_commit/commands/validate_config.py
Normal file
18
pre_commit/commands/validate_config.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import clientlib
|
||||||
|
|
||||||
|
|
||||||
|
def validate_config(filenames: Sequence[str]) -> int:
|
||||||
|
ret = 0
|
||||||
|
|
||||||
|
for filename in filenames:
|
||||||
|
try:
|
||||||
|
clientlib.load_config(filename)
|
||||||
|
except clientlib.InvalidConfigError as e:
|
||||||
|
print(e)
|
||||||
|
ret = 1
|
||||||
|
|
||||||
|
return ret
|
||||||
18
pre_commit/commands/validate_manifest.py
Normal file
18
pre_commit/commands/validate_manifest.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import clientlib
|
||||||
|
|
||||||
|
|
||||||
|
def validate_manifest(filenames: Sequence[str]) -> int:
|
||||||
|
ret = 0
|
||||||
|
|
||||||
|
for filename in filenames:
|
||||||
|
try:
|
||||||
|
clientlib.load_manifest(filename)
|
||||||
|
except clientlib.InvalidManifestError as e:
|
||||||
|
print(e)
|
||||||
|
ret = 1
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
@ -1,25 +1,13 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import pkg_resources
|
import importlib.metadata
|
||||||
|
|
||||||
CONFIG_FILE = '.pre-commit-config.yaml'
|
CONFIG_FILE = '.pre-commit-config.yaml'
|
||||||
MANIFEST_FILE = '.pre-commit-hooks.yaml'
|
MANIFEST_FILE = '.pre-commit-hooks.yaml'
|
||||||
|
|
||||||
YAML_DUMP_KWARGS = {
|
|
||||||
'default_flow_style': False,
|
|
||||||
# Use unicode
|
|
||||||
'encoding': None,
|
|
||||||
'indent': 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Bump when installation changes in a backwards / forwards incompatible way
|
|
||||||
INSTALLED_STATE_VERSION = '1'
|
|
||||||
# Bump when modifying `empty_template`
|
# Bump when modifying `empty_template`
|
||||||
LOCAL_REPO_VERSION = '1'
|
LOCAL_REPO_VERSION = '1'
|
||||||
|
|
||||||
VERSION = pkg_resources.get_distribution('pre-commit').version
|
VERSION = importlib.metadata.version('pre_commit')
|
||||||
VERSION_PARSED = pkg_resources.parse_version(VERSION)
|
|
||||||
|
|
||||||
# `manual` is not invoked by any installed git hook. See #719
|
DEFAULT = 'default'
|
||||||
STAGES = ('commit', 'commit-msg', 'manual', 'push')
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import enum
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import MutableMapping
|
||||||
|
from typing import NamedTuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
_Unset = enum.Enum('_Unset', 'UNSET')
|
||||||
|
UNSET = _Unset.UNSET
|
||||||
|
|
||||||
|
|
||||||
UNSET = collections.namedtuple('UNSET', ())()
|
class Var(NamedTuple):
|
||||||
|
name: str
|
||||||
|
default: str = ''
|
||||||
|
|
||||||
|
|
||||||
Var = collections.namedtuple('Var', ('name', 'default'))
|
SubstitutionT = tuple[Union[str, Var], ...]
|
||||||
Var.__new__.__defaults__ = ('',)
|
ValueT = Union[str, _Unset, SubstitutionT]
|
||||||
|
PatchesT = tuple[tuple[str, ValueT], ...]
|
||||||
|
|
||||||
|
|
||||||
def format_env(parts, env):
|
def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
|
||||||
return ''.join(
|
return ''.join(
|
||||||
env.get(part.name, part.default) if isinstance(part, Var) else part
|
env.get(part.name, part.default) if isinstance(part, Var) else part
|
||||||
for part in parts
|
for part in parts
|
||||||
|
|
@ -21,7 +30,10 @@ def format_env(parts, env):
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def envcontext(patch, _env=None):
|
def envcontext(
|
||||||
|
patch: PatchesT,
|
||||||
|
_env: MutableMapping[str, str] | None = None,
|
||||||
|
) -> Generator[None]:
|
||||||
"""In this context, `os.environ` is modified according to `patch`.
|
"""In this context, `os.environ` is modified according to `patch`.
|
||||||
|
|
||||||
`patch` is an iterable of 2-tuples (key, value):
|
`patch` is an iterable of 2-tuples (key, value):
|
||||||
|
|
@ -33,7 +45,7 @@ def envcontext(patch, _env=None):
|
||||||
replaced with the previous environment
|
replaced with the previous environment
|
||||||
"""
|
"""
|
||||||
env = os.environ if _env is None else _env
|
env = os.environ if _env is None else _env
|
||||||
before = env.copy()
|
before = dict(env)
|
||||||
|
|
||||||
for k, v in patch:
|
for k, v in patch:
|
||||||
if v is UNSET:
|
if v is UNSET:
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,81 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import os.path
|
import os.path
|
||||||
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
import six
|
import pre_commit.constants as C
|
||||||
|
|
||||||
from pre_commit import five
|
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
|
from pre_commit.errors import FatalError
|
||||||
from pre_commit.store import Store
|
from pre_commit.store import Store
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
from pre_commit.util import force_bytes
|
||||||
|
|
||||||
|
|
||||||
class FatalError(RuntimeError):
|
def _log_and_exit(
|
||||||
pass
|
msg: str,
|
||||||
|
ret_code: int,
|
||||||
|
exc: BaseException,
|
||||||
|
formatted: str,
|
||||||
|
) -> None:
|
||||||
|
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
|
||||||
|
output.write_line_b(error_msg)
|
||||||
|
|
||||||
|
_, git_version_b, _ = cmd_output_b('git', '--version', check=False)
|
||||||
|
git_version = git_version_b.decode(errors='backslashreplace').rstrip()
|
||||||
|
|
||||||
def _to_bytes(exc):
|
storedir = Store().directory
|
||||||
try:
|
log_path = os.path.join(storedir, 'pre-commit.log')
|
||||||
return bytes(exc)
|
with contextlib.ExitStack() as ctx:
|
||||||
except Exception:
|
if os.access(storedir, os.W_OK):
|
||||||
return six.text_type(exc).encode('UTF-8')
|
output.write_line(f'Check the log at {log_path}')
|
||||||
|
log: IO[bytes] = ctx.enter_context(open(log_path, 'wb'))
|
||||||
|
else: # pragma: win32 no cover
|
||||||
|
output.write_line(f'Failed to write to log at {log_path}')
|
||||||
|
log = sys.stdout.buffer
|
||||||
|
|
||||||
|
_log_line = functools.partial(output.write_line, stream=log)
|
||||||
|
_log_line_b = functools.partial(output.write_line_b, stream=log)
|
||||||
|
|
||||||
def _log_and_exit(msg, exc, formatted):
|
_log_line('### version information')
|
||||||
error_msg = b''.join((
|
_log_line()
|
||||||
five.to_bytes(msg), b': ',
|
_log_line('```')
|
||||||
five.to_bytes(type(exc).__name__), b': ',
|
_log_line(f'pre-commit version: {C.VERSION}')
|
||||||
_to_bytes(exc), b'\n',
|
_log_line(f'git --version: {git_version}')
|
||||||
))
|
_log_line('sys.version:')
|
||||||
output.write(error_msg)
|
for line in sys.version.splitlines():
|
||||||
store = Store()
|
_log_line(f' {line}')
|
||||||
store.require_created()
|
_log_line(f'sys.executable: {sys.executable}')
|
||||||
log_path = os.path.join(store.directory, 'pre-commit.log')
|
_log_line(f'os.name: {os.name}')
|
||||||
output.write_line('Check the log at {}'.format(log_path))
|
_log_line(f'sys.platform: {sys.platform}')
|
||||||
with open(log_path, 'wb') as log:
|
_log_line('```')
|
||||||
output.write(error_msg, stream=log)
|
_log_line()
|
||||||
output.write_line(formatted, stream=log)
|
|
||||||
raise SystemExit(1)
|
_log_line('### error information')
|
||||||
|
_log_line()
|
||||||
|
_log_line('```')
|
||||||
|
_log_line_b(error_msg)
|
||||||
|
_log_line('```')
|
||||||
|
_log_line()
|
||||||
|
_log_line('```')
|
||||||
|
_log_line(formatted.rstrip())
|
||||||
|
_log_line('```')
|
||||||
|
raise SystemExit(ret_code)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def error_handler():
|
def error_handler() -> Generator[None]:
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except FatalError as e:
|
except (Exception, KeyboardInterrupt) as e:
|
||||||
_log_and_exit('An error has occurred', e, traceback.format_exc())
|
if isinstance(e, FatalError):
|
||||||
except Exception as e:
|
msg, ret_code = 'An error has occurred', 1
|
||||||
_log_and_exit(
|
elif isinstance(e, KeyboardInterrupt):
|
||||||
'An unexpected error has occurred', e, traceback.format_exc(),
|
msg, ret_code = 'Interrupted (^C)', 130
|
||||||
)
|
else:
|
||||||
|
msg, ret_code = 'An unexpected error has occurred', 3
|
||||||
|
_log_and_exit(msg, ret_code, e, traceback.format_exc())
|
||||||
|
|
|
||||||
5
pre_commit/errors.py
Normal file
5
pre_commit/errors.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class FatalError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
|
||||||
try: # pragma: no cover (windows)
|
if sys.platform == 'win32': # pragma: no cover (windows)
|
||||||
import msvcrt
|
import msvcrt
|
||||||
|
|
||||||
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
|
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
|
||||||
|
|
@ -15,15 +17,18 @@ try: # pragma: no cover (windows)
|
||||||
_region = 0xffff
|
_region = 0xffff
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _locked(fileno, blocked_cb):
|
def _locked(
|
||||||
|
fileno: int,
|
||||||
|
blocked_cb: Callable[[], None],
|
||||||
|
) -> Generator[None]:
|
||||||
try:
|
try:
|
||||||
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
|
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
|
||||||
except IOError:
|
except OSError:
|
||||||
blocked_cb()
|
blocked_cb()
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
|
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
|
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
|
||||||
# flag is specified and the file cannot be locked after 10
|
# flag is specified and the file cannot be locked after 10
|
||||||
# attempts.
|
# attempts.
|
||||||
|
|
@ -41,14 +46,17 @@ try: # pragma: no cover (windows)
|
||||||
# "Regions should be locked only briefly and should be unlocked
|
# "Regions should be locked only briefly and should be unlocked
|
||||||
# before closing a file or exiting the program."
|
# before closing a file or exiting the program."
|
||||||
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
|
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
|
||||||
except ImportError: # pragma: no cover (posix)
|
else: # pragma: win32 no cover
|
||||||
import fcntl
|
import fcntl
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def _locked(fileno, blocked_cb):
|
def _locked(
|
||||||
|
fileno: int,
|
||||||
|
blocked_cb: Callable[[], None],
|
||||||
|
) -> Generator[None]:
|
||||||
try:
|
try:
|
||||||
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except IOError:
|
except OSError: # pragma: no cover (tests are single-threaded)
|
||||||
blocked_cb()
|
blocked_cb()
|
||||||
fcntl.flock(fileno, fcntl.LOCK_EX)
|
fcntl.flock(fileno, fcntl.LOCK_EX)
|
||||||
try:
|
try:
|
||||||
|
|
@ -58,7 +66,10 @@ except ImportError: # pragma: no cover (posix)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def lock(path, blocked_cb):
|
def lock(
|
||||||
|
path: str,
|
||||||
|
blocked_cb: Callable[[], None],
|
||||||
|
) -> Generator[None]:
|
||||||
with open(path, 'a+') as f:
|
with open(path, 'a+') as f:
|
||||||
with _locked(f.fileno(), blocked_cb):
|
with _locked(f.fileno(), blocked_cb):
|
||||||
yield
|
yield
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
def to_text(s):
|
|
||||||
return s if isinstance(s, six.text_type) else s.decode('UTF-8')
|
|
||||||
|
|
||||||
|
|
||||||
def to_bytes(s):
|
|
||||||
return s if isinstance(s, bytes) else s.encode('UTF-8')
|
|
||||||
|
|
||||||
|
|
||||||
n = to_bytes if six.PY2 else to_text
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
from pre_commit.error_handler import FatalError
|
from pre_commit.errors import FatalError
|
||||||
from pre_commit.util import CalledProcessError
|
from pre_commit.util import CalledProcessError
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# see #2046
|
||||||
|
NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false')
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('pre_commit')
|
def zsplit(s: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def zsplit(s):
|
|
||||||
s = s.strip('\0')
|
s = s.strip('\0')
|
||||||
if s:
|
if s:
|
||||||
return s.split('\0')
|
return s.split('\0')
|
||||||
|
|
@ -20,29 +24,76 @@ def zsplit(s):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def get_root():
|
def no_git_env(_env: Mapping[str, str] | None = None) -> dict[str, str]:
|
||||||
|
# Too many bugs dealing with environment variables and GIT:
|
||||||
|
# https://github.com/pre-commit/pre-commit/issues/300
|
||||||
|
# In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running
|
||||||
|
# pre-commit hooks
|
||||||
|
# In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE
|
||||||
|
# while running pre-commit hooks in submodules.
|
||||||
|
# GIT_DIR: Causes git clone to clone wrong thing
|
||||||
|
# GIT_INDEX_FILE: Causes 'error invalid object ...' during commit
|
||||||
|
_env = _env if _env is not None else os.environ
|
||||||
|
return {
|
||||||
|
k: v for k, v in _env.items()
|
||||||
|
if not k.startswith('GIT_') or
|
||||||
|
k.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or
|
||||||
|
k in {
|
||||||
|
'GIT_EXEC_PATH', 'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSL_CAINFO',
|
||||||
|
'GIT_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT',
|
||||||
|
'GIT_HTTP_PROXY_AUTHMETHOD',
|
||||||
|
'GIT_ALLOW_PROTOCOL',
|
||||||
|
'GIT_ASKPASS',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_root() -> str:
|
||||||
|
# Git 2.25 introduced a change to "rev-parse --show-toplevel" that exposed
|
||||||
|
# underlying volumes for Windows drives mapped with SUBST. We use
|
||||||
|
# "rev-parse --show-cdup" to get the appropriate path, but must perform
|
||||||
|
# an extra check to see if we are in the .git directory.
|
||||||
try:
|
try:
|
||||||
return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip()
|
root = os.path.abspath(
|
||||||
|
cmd_output('git', 'rev-parse', '--show-cdup')[1].strip(),
|
||||||
|
)
|
||||||
|
inside_git_dir = cmd_output(
|
||||||
|
'git', 'rev-parse', '--is-inside-git-dir',
|
||||||
|
)[1].strip()
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
raise FatalError(
|
raise FatalError(
|
||||||
'git failed. Is it installed, and are you in a Git repository '
|
'git failed. Is it installed, and are you in a Git repository '
|
||||||
'directory?',
|
'directory?',
|
||||||
)
|
)
|
||||||
|
if inside_git_dir != 'false':
|
||||||
|
raise FatalError(
|
||||||
|
'git toplevel unexpectedly empty! make sure you are not '
|
||||||
|
'inside the `.git` directory of your repository.',
|
||||||
|
)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
def get_git_dir(git_root):
|
def get_git_dir(git_root: str = '.') -> str:
|
||||||
return os.path.normpath(os.path.join(
|
opt = '--git-dir'
|
||||||
git_root,
|
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root)
|
||||||
cmd_output('git', 'rev-parse', '--git-dir', cwd=git_root)[1].strip(),
|
git_dir = out.strip()
|
||||||
))
|
if git_dir != opt:
|
||||||
|
return os.path.normpath(os.path.join(git_root, git_dir))
|
||||||
|
else:
|
||||||
|
raise AssertionError('unreachable: no git dir')
|
||||||
|
|
||||||
|
|
||||||
def get_remote_url(git_root):
|
def get_git_common_dir(git_root: str = '.') -> str:
|
||||||
ret = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)[1]
|
opt = '--git-common-dir'
|
||||||
return ret.strip()
|
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root)
|
||||||
|
git_common_dir = out.strip()
|
||||||
|
if git_common_dir != opt:
|
||||||
|
return os.path.normpath(os.path.join(git_root, git_common_dir))
|
||||||
|
else: # pragma: no cover (git < 2.5)
|
||||||
|
return get_git_dir(git_root)
|
||||||
|
|
||||||
|
|
||||||
def is_in_merge_conflict():
|
def is_in_merge_conflict() -> bool:
|
||||||
git_dir = get_git_dir('.')
|
git_dir = get_git_dir('.')
|
||||||
return (
|
return (
|
||||||
os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and
|
os.path.exists(os.path.join(git_dir, 'MERGE_MSG')) and
|
||||||
|
|
@ -50,75 +101,145 @@ def is_in_merge_conflict():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_merge_msg_for_conflicts(merge_msg):
|
def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]:
|
||||||
# Conflicted files start with tabs
|
# Conflicted files start with tabs
|
||||||
return [
|
return [
|
||||||
line.lstrip(b'#').strip().decode('UTF-8')
|
line.lstrip(b'#').strip().decode()
|
||||||
for line in merge_msg.splitlines()
|
for line in merge_msg.splitlines()
|
||||||
# '#\t' for git 2.4.1
|
# '#\t' for git 2.4.1
|
||||||
if line.startswith((b'\t', b'#\t'))
|
if line.startswith((b'\t', b'#\t'))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_conflicted_files():
|
def get_conflicted_files() -> set[str]:
|
||||||
logger.info('Checking merge-conflict files only.')
|
logger.info('Checking merge-conflict files only.')
|
||||||
# Need to get the conflicted files from the MERGE_MSG because they could
|
# Need to get the conflicted files from the MERGE_MSG because they could
|
||||||
# have resolved the conflict by choosing one side or the other
|
# have resolved the conflict by choosing one side or the other
|
||||||
merge_msg = open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb').read()
|
with open(os.path.join(get_git_dir('.'), 'MERGE_MSG'), 'rb') as f:
|
||||||
|
merge_msg = f.read()
|
||||||
merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg)
|
merge_conflict_filenames = parse_merge_msg_for_conflicts(merge_msg)
|
||||||
|
|
||||||
# This will get the rest of the changes made after the merge.
|
# This will get the rest of the changes made after the merge.
|
||||||
# If they resolved the merge conflict by choosing a mesh of both sides
|
# If they resolved the merge conflict by choosing a mesh of both sides
|
||||||
# this will also include the conflicted files
|
# this will also include the conflicted files
|
||||||
tree_hash = cmd_output('git', 'write-tree')[1].strip()
|
tree_hash = cmd_output('git', 'write-tree')[1].strip()
|
||||||
merge_diff_filenames = zsplit(cmd_output(
|
merge_diff_filenames = zsplit(
|
||||||
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
|
cmd_output(
|
||||||
'-m', tree_hash, 'HEAD', 'MERGE_HEAD',
|
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
|
||||||
)[1])
|
'-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
|
||||||
|
)[1],
|
||||||
|
)
|
||||||
return set(merge_conflict_filenames) | set(merge_diff_filenames)
|
return set(merge_conflict_filenames) | set(merge_diff_filenames)
|
||||||
|
|
||||||
|
|
||||||
def get_staged_files():
|
def get_staged_files(cwd: str | None = None) -> list[str]:
|
||||||
return zsplit(cmd_output(
|
return zsplit(
|
||||||
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
|
cmd_output(
|
||||||
# Everything except for D
|
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
|
||||||
'--diff-filter=ACMRTUXB',
|
# Everything except for D
|
||||||
)[1])
|
'--diff-filter=ACMRTUXB',
|
||||||
|
cwd=cwd,
|
||||||
|
)[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_all_files():
|
def intent_to_add_files() -> list[str]:
|
||||||
|
_, stdout, _ = cmd_output(
|
||||||
|
'git', 'diff', '--no-ext-diff', '--ignore-submodules',
|
||||||
|
'--diff-filter=A', '--name-only', '-z',
|
||||||
|
)
|
||||||
|
return zsplit(stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_files() -> list[str]:
|
||||||
return zsplit(cmd_output('git', 'ls-files', '-z')[1])
|
return zsplit(cmd_output('git', 'ls-files', '-z')[1])
|
||||||
|
|
||||||
|
|
||||||
def get_changed_files(new, old):
|
def get_changed_files(old: str, new: str) -> list[str]:
|
||||||
return zsplit(cmd_output(
|
diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z')
|
||||||
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
|
try:
|
||||||
'{}...{}'.format(old, new),
|
_, out, _ = cmd_output(*diff_cmd, f'{old}...{new}')
|
||||||
)[1])
|
except CalledProcessError: # pragma: no cover (new git)
|
||||||
|
# on newer git where old and new do not have a merge base git fails
|
||||||
|
# so we try a full diff (this is what old git did for us!)
|
||||||
|
_, out, _ = cmd_output(*diff_cmd, f'{old}..{new}')
|
||||||
|
|
||||||
|
return zsplit(out)
|
||||||
|
|
||||||
|
|
||||||
def head_rev(remote):
|
def head_rev(remote: str) -> str:
|
||||||
_, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD')
|
_, out, _ = cmd_output('git', 'ls-remote', '--exit-code', remote, 'HEAD')
|
||||||
return out.split()[0]
|
return out.split()[0]
|
||||||
|
|
||||||
|
|
||||||
def check_for_cygwin_mismatch():
|
def has_diff(*args: str, repo: str = '.') -> bool:
|
||||||
|
cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args)
|
||||||
|
return cmd_output_b(*cmd, cwd=repo, check=False)[0] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def has_core_hookpaths_set() -> bool:
|
||||||
|
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False)
|
||||||
|
return bool(out.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def init_repo(path: str, remote: str) -> None:
|
||||||
|
if os.path.isdir(remote):
|
||||||
|
remote = os.path.abspath(remote)
|
||||||
|
|
||||||
|
git = ('git', *NO_FS_MONITOR)
|
||||||
|
env = no_git_env()
|
||||||
|
# avoid the user's template so that hooks do not recurse
|
||||||
|
cmd_output_b(*git, 'init', '--template=', path, env=env)
|
||||||
|
cmd_output_b(*git, 'remote', 'add', 'origin', remote, cwd=path, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def commit(repo: str = '.') -> None:
|
||||||
|
env = no_git_env()
|
||||||
|
name, email = 'pre-commit', 'asottile+pre-commit@umich.edu'
|
||||||
|
env['GIT_AUTHOR_NAME'] = env['GIT_COMMITTER_NAME'] = name
|
||||||
|
env['GIT_AUTHOR_EMAIL'] = env['GIT_COMMITTER_EMAIL'] = email
|
||||||
|
cmd = ('git', 'commit', '--no-edit', '--no-gpg-sign', '-n', '-minit')
|
||||||
|
cmd_output_b(*cmd, cwd=repo, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def git_path(name: str, repo: str = '.') -> str:
|
||||||
|
_, out, _ = cmd_output('git', 'rev-parse', '--git-path', name, cwd=repo)
|
||||||
|
return os.path.join(repo, out.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_cygwin_mismatch() -> None:
|
||||||
"""See https://github.com/pre-commit/pre-commit/issues/354"""
|
"""See https://github.com/pre-commit/pre-commit/issues/354"""
|
||||||
if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows)
|
if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows)
|
||||||
is_cygwin_python = sys.platform == 'cygwin'
|
is_cygwin_python = sys.platform == 'cygwin'
|
||||||
toplevel = cmd_output('git', 'rev-parse', '--show-toplevel')[1]
|
try:
|
||||||
|
toplevel = get_root()
|
||||||
|
except FatalError: # skip the check if we're not in a git repo
|
||||||
|
return
|
||||||
is_cygwin_git = toplevel.startswith('/')
|
is_cygwin_git = toplevel.startswith('/')
|
||||||
|
|
||||||
if is_cygwin_python ^ is_cygwin_git:
|
if is_cygwin_python ^ is_cygwin_git:
|
||||||
exe_type = {True: '(cygwin)', False: '(windows)'}
|
exe_type = {True: '(cygwin)', False: '(windows)'}
|
||||||
logger.warn(
|
logger.warning(
|
||||||
'pre-commit has detected a mix of cygwin python / git\n'
|
f'pre-commit has detected a mix of cygwin python / git\n'
|
||||||
'This combination is not supported, it is likely you will '
|
f'This combination is not supported, it is likely you will '
|
||||||
'receive an error later in the program.\n'
|
f'receive an error later in the program.\n'
|
||||||
'Make sure to use cygwin git+python while using cygwin\n'
|
f'Make sure to use cygwin git+python while using cygwin\n'
|
||||||
'These can be installed through the cygwin installer.\n'
|
f'These can be installed through the cygwin installer.\n'
|
||||||
' - python {}\n'
|
f' - python {exe_type[is_cygwin_python]}\n'
|
||||||
' - git {}\n'.format(
|
f' - git {exe_type[is_cygwin_git]}\n',
|
||||||
exe_type[is_cygwin_python], exe_type[is_cygwin_git],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_candidate_tag(rev: str, git_repo: str) -> str:
|
||||||
|
"""Get the best tag candidate.
|
||||||
|
|
||||||
|
Multiple tags can exist on a SHA. Sometimes a moving tag is attached
|
||||||
|
to a version tag. Try to pick the tag that looks like a version.
|
||||||
|
"""
|
||||||
|
tags = cmd_output(
|
||||||
|
'git', *NO_FS_MONITOR, 'tag', '--points-at', rev, cwd=git_repo,
|
||||||
|
)[1].splitlines()
|
||||||
|
for tag in tags:
|
||||||
|
if '.' in tag:
|
||||||
|
return tag
|
||||||
|
return rev
|
||||||
|
|
|
||||||
60
pre_commit/hook.py
Normal file
60
pre_commit/hook.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
|
|
||||||
|
class Hook(NamedTuple):
|
||||||
|
src: str
|
||||||
|
prefix: Prefix
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
entry: str
|
||||||
|
language: str
|
||||||
|
alias: str
|
||||||
|
files: str
|
||||||
|
exclude: str
|
||||||
|
types: Sequence[str]
|
||||||
|
types_or: Sequence[str]
|
||||||
|
exclude_types: Sequence[str]
|
||||||
|
additional_dependencies: Sequence[str]
|
||||||
|
args: Sequence[str]
|
||||||
|
always_run: bool
|
||||||
|
fail_fast: bool
|
||||||
|
pass_filenames: bool
|
||||||
|
description: str
|
||||||
|
language_version: str
|
||||||
|
log_file: str
|
||||||
|
minimum_pre_commit_version: str
|
||||||
|
require_serial: bool
|
||||||
|
stages: Sequence[str]
|
||||||
|
verbose: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]:
|
||||||
|
return (
|
||||||
|
self.prefix,
|
||||||
|
self.language,
|
||||||
|
self.language_version,
|
||||||
|
tuple(self.additional_dependencies),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, src: str, prefix: Prefix, dct: dict[str, Any]) -> Hook:
|
||||||
|
# TODO: have cfgv do this (?)
|
||||||
|
extra_keys = set(dct) - _KEYS
|
||||||
|
if extra_keys:
|
||||||
|
logger.warning(
|
||||||
|
f'Unexpected key(s) present on {src} => {dct["id"]}: '
|
||||||
|
f'{", ".join(sorted(extra_keys))}',
|
||||||
|
)
|
||||||
|
return cls(src=src, prefix=prefix, **{k: dct[k] for k in _KEYS})
|
||||||
|
|
||||||
|
|
||||||
|
_KEYS = frozenset(set(Hook._fields) - {'src', 'prefix'})
|
||||||
196
pre_commit/lang_base.py
Normal file
196
pre_commit/lang_base.py
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
from typing import ContextManager
|
||||||
|
from typing import NoReturn
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import parse_shebang
|
||||||
|
from pre_commit import xargs
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
|
||||||
|
FIXED_RANDOM_SEED = 1542676187
|
||||||
|
|
||||||
|
SHIMS_RE = re.compile(r'[/\\]shims[/\\]')
|
||||||
|
|
||||||
|
|
||||||
|
class Language(Protocol):
|
||||||
|
# Use `None` for no installation / environment
|
||||||
|
@property
|
||||||
|
def ENVIRONMENT_DIR(self) -> str | None: ...
|
||||||
|
# return a value to replace `'default` for `language_version`
|
||||||
|
def get_default_version(self) -> str: ...
|
||||||
|
# return whether the environment is healthy (or should be rebuilt)
|
||||||
|
def health_check(self, prefix: Prefix, version: str) -> str | None: ...
|
||||||
|
|
||||||
|
# install a repository for the given language and language_version
|
||||||
|
def install_environment(
|
||||||
|
self,
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
# modify the environment for hook execution
|
||||||
|
def in_env(self, prefix: Prefix, version: str) -> ContextManager[None]: ...
|
||||||
|
|
||||||
|
# execute a hook and return the exit code and output
|
||||||
|
def run_hook(
|
||||||
|
self,
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def exe_exists(exe: str) -> bool:
|
||||||
|
found = parse_shebang.find_executable(exe)
|
||||||
|
if found is None: # exe exists
|
||||||
|
return False
|
||||||
|
|
||||||
|
homedir = os.path.expanduser('~')
|
||||||
|
try:
|
||||||
|
common: str | None = os.path.commonpath((found, homedir))
|
||||||
|
except ValueError: # on windows, different drives raises ValueError
|
||||||
|
common = None
|
||||||
|
|
||||||
|
return (
|
||||||
|
# it is not in a /shims/ directory
|
||||||
|
not SHIMS_RE.search(found) and
|
||||||
|
(
|
||||||
|
# the homedir is / (docker, service user, etc.)
|
||||||
|
os.path.dirname(homedir) == homedir or
|
||||||
|
# the exe is not contained in the home directory
|
||||||
|
common != homedir
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cmd(prefix: Prefix, cmd: tuple[str, ...], **kwargs: Any) -> None:
|
||||||
|
cmd_output_b(*cmd, cwd=prefix.prefix_dir, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def environment_dir(prefix: Prefix, d: str, language_version: str) -> str:
|
||||||
|
return prefix.path(f'{d}-{language_version}')
|
||||||
|
|
||||||
|
|
||||||
|
def assert_version_default(binary: str, version: str) -> None:
|
||||||
|
if version != C.DEFAULT:
|
||||||
|
raise AssertionError(
|
||||||
|
f'for now, pre-commit requires system-installed {binary} -- '
|
||||||
|
f'you selected `language_version: {version}`',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_no_additional_deps(
|
||||||
|
lang: str,
|
||||||
|
additional_deps: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
if additional_deps:
|
||||||
|
raise AssertionError(
|
||||||
|
f'for now, pre-commit does not support '
|
||||||
|
f'additional_dependencies for {lang} -- '
|
||||||
|
f'you selected `additional_dependencies: {additional_deps}`',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def basic_get_default_version() -> str:
|
||||||
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def basic_health_check(prefix: Prefix, language_version: str) -> str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def no_install(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> NoReturn:
|
||||||
|
raise AssertionError('This language is not installable')
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def no_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def target_concurrency() -> int:
|
||||||
|
if 'PRE_COMMIT_NO_CONCURRENCY' in os.environ:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
# Travis appears to have a bunch of CPUs, but we can't use them all.
|
||||||
|
if 'TRAVIS' in os.environ:
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return xargs.cpu_count()
|
||||||
|
|
||||||
|
|
||||||
|
def _shuffled(seq: Sequence[str]) -> list[str]:
|
||||||
|
"""Deterministically shuffle"""
|
||||||
|
fixed_random = random.Random()
|
||||||
|
fixed_random.seed(FIXED_RANDOM_SEED, version=1)
|
||||||
|
|
||||||
|
seq = list(seq)
|
||||||
|
fixed_random.shuffle(seq)
|
||||||
|
return seq
|
||||||
|
|
||||||
|
|
||||||
|
def run_xargs(
|
||||||
|
cmd: tuple[str, ...],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
if require_serial:
|
||||||
|
jobs = 1
|
||||||
|
else:
|
||||||
|
# Shuffle the files so that they more evenly fill out the xargs
|
||||||
|
# partitions, but do it deterministically in case a hook cares about
|
||||||
|
# ordering.
|
||||||
|
file_args = _shuffled(file_args)
|
||||||
|
jobs = target_concurrency()
|
||||||
|
return xargs.xargs(cmd, file_args, target_concurrency=jobs, color=color)
|
||||||
|
|
||||||
|
|
||||||
|
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
|
||||||
|
cmd = shlex.split(entry)
|
||||||
|
if cmd[:2] == ['pre-commit', 'hazmat']:
|
||||||
|
cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
|
||||||
|
return (*cmd, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def basic_run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
return run_xargs(
|
||||||
|
hook_cmd(entry, args),
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from pre_commit.languages import docker
|
|
||||||
from pre_commit.languages import docker_image
|
|
||||||
from pre_commit.languages import golang
|
|
||||||
from pre_commit.languages import node
|
|
||||||
from pre_commit.languages import pcre
|
|
||||||
from pre_commit.languages import pygrep
|
|
||||||
from pre_commit.languages import python
|
|
||||||
from pre_commit.languages import python_venv
|
|
||||||
from pre_commit.languages import ruby
|
|
||||||
from pre_commit.languages import rust
|
|
||||||
from pre_commit.languages import script
|
|
||||||
from pre_commit.languages import swift
|
|
||||||
from pre_commit.languages import system
|
|
||||||
|
|
||||||
# A language implements the following constant and functions in its module:
|
|
||||||
#
|
|
||||||
# # Use None for no environment
|
|
||||||
# ENVIRONMENT_DIR = 'foo_env'
|
|
||||||
#
|
|
||||||
# def get_default_version():
|
|
||||||
# """Return a value to replace the 'default' value for language_version.
|
|
||||||
#
|
|
||||||
# return 'default' if there is no better option.
|
|
||||||
# """
|
|
||||||
#
|
|
||||||
# def healthy(prefix, language_version):
|
|
||||||
# """Return whether or not the environment is considered functional."""
|
|
||||||
#
|
|
||||||
# def install_environment(prefix, version, additional_dependencies):
|
|
||||||
# """Installs a repository in the given repository. Note that the current
|
|
||||||
# working directory will already be inside the repository.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# prefix - `Prefix` bound to the repository.
|
|
||||||
# version - A version specified in the hook configuration or
|
|
||||||
# 'default'.
|
|
||||||
# """
|
|
||||||
#
|
|
||||||
# def run_hook(prefix, hook, file_args):
|
|
||||||
# """Runs a hook and returns the returncode and output of running that
|
|
||||||
# hook.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# prefix - `Prefix` bound to the repository.
|
|
||||||
# hook - Hook dictionary
|
|
||||||
# file_args - The files to be run
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# (returncode, stdout, stderr)
|
|
||||||
# """
|
|
||||||
|
|
||||||
languages = {
|
|
||||||
'docker': docker,
|
|
||||||
'docker_image': docker_image,
|
|
||||||
'golang': golang,
|
|
||||||
'node': node,
|
|
||||||
'pcre': pcre,
|
|
||||||
'pygrep': pygrep,
|
|
||||||
'python': python,
|
|
||||||
'python_venv': python_venv,
|
|
||||||
'ruby': ruby,
|
|
||||||
'rust': rust,
|
|
||||||
'script': script,
|
|
||||||
'swift': swift,
|
|
||||||
'system': system,
|
|
||||||
}
|
|
||||||
all_languages = sorted(languages)
|
|
||||||
77
pre_commit/languages/conda.py
Normal file
77
pre_commit/languages/conda.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import SubstitutionT
|
||||||
|
from pre_commit.envcontext import UNSET
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'conda'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(env: str) -> PatchesT:
|
||||||
|
# On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows
|
||||||
|
# they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin,
|
||||||
|
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
|
||||||
|
# seems to be used for python.exe.
|
||||||
|
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
|
||||||
|
if sys.platform == 'win32': # pragma: win32 cover
|
||||||
|
path = (env, os.pathsep, *path)
|
||||||
|
path = (os.path.join(env, 'Scripts'), os.pathsep, *path)
|
||||||
|
path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path)
|
||||||
|
|
||||||
|
return (
|
||||||
|
('PYTHONHOME', UNSET),
|
||||||
|
('VIRTUAL_ENV', UNSET),
|
||||||
|
('CONDA_PREFIX', env),
|
||||||
|
('PATH', path),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _conda_exe() -> str:
|
||||||
|
if os.environ.get('PRE_COMMIT_USE_MICROMAMBA'):
|
||||||
|
return 'micromamba'
|
||||||
|
elif os.environ.get('PRE_COMMIT_USE_MAMBA'):
|
||||||
|
return 'mamba'
|
||||||
|
else:
|
||||||
|
return 'conda'
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('conda', version)
|
||||||
|
|
||||||
|
conda_exe = _conda_exe()
|
||||||
|
|
||||||
|
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
cmd_output_b(
|
||||||
|
conda_exe, 'env', 'create', '-p', env_dir, '--file',
|
||||||
|
'environment.yml', cwd=prefix.prefix_dir,
|
||||||
|
)
|
||||||
|
if additional_dependencies:
|
||||||
|
cmd_output_b(
|
||||||
|
conda_exe, 'install', '-p', env_dir, *additional_dependencies,
|
||||||
|
cwd=prefix.prefix_dir,
|
||||||
|
)
|
||||||
76
pre_commit/languages/coursier.py
Normal file
76
pre_commit/languages/coursier.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os.path
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.errors import FatalError
|
||||||
|
from pre_commit.parse_shebang import find_executable
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'coursier'
|
||||||
|
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('coursier', version)
|
||||||
|
|
||||||
|
# Support both possible executable names (either "cs" or "coursier")
|
||||||
|
cs = find_executable('cs') or find_executable('coursier')
|
||||||
|
if cs is None:
|
||||||
|
raise AssertionError(
|
||||||
|
'pre-commit requires system-installed "cs" or "coursier" '
|
||||||
|
'executables in the application search path',
|
||||||
|
)
|
||||||
|
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
|
def _install(*opts: str) -> None:
|
||||||
|
assert cs is not None
|
||||||
|
lang_base.setup_cmd(prefix, (cs, 'fetch', *opts))
|
||||||
|
lang_base.setup_cmd(prefix, (cs, 'install', '--dir', envdir, *opts))
|
||||||
|
|
||||||
|
with in_env(prefix, version):
|
||||||
|
channel = prefix.path('.pre-commit-channel')
|
||||||
|
if os.path.isdir(channel):
|
||||||
|
for app_descriptor in os.listdir(channel):
|
||||||
|
_, app_file = os.path.split(app_descriptor)
|
||||||
|
app, _ = os.path.splitext(app_file)
|
||||||
|
_install(
|
||||||
|
'--default-channels=false',
|
||||||
|
'--channel', channel,
|
||||||
|
app,
|
||||||
|
)
|
||||||
|
elif not additional_dependencies:
|
||||||
|
raise FatalError(
|
||||||
|
'expected .pre-commit-channel dir or additional_dependencies',
|
||||||
|
)
|
||||||
|
|
||||||
|
if additional_dependencies:
|
||||||
|
_install(*additional_dependencies)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(target_dir: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('PATH', (target_dir, os.pathsep, Var('PATH'))),
|
||||||
|
('COURSIER_CACHE', os.path.join(target_dir, '.cs-cache')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
97
pre_commit/languages/dart.py
Normal file
97
pre_commit/languages/dart.py
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import win_exe
|
||||||
|
from pre_commit.yaml import yaml_load
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'dartenv'
|
||||||
|
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('dart', version)
|
||||||
|
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
bin_dir = os.path.join(envdir, 'bin')
|
||||||
|
|
||||||
|
def _install_dir(prefix_p: Prefix, pub_cache: str) -> None:
|
||||||
|
dart_env = {**os.environ, 'PUB_CACHE': pub_cache}
|
||||||
|
|
||||||
|
with open(prefix_p.path('pubspec.yaml')) as f:
|
||||||
|
pubspec_contents = yaml_load(f)
|
||||||
|
|
||||||
|
lang_base.setup_cmd(prefix_p, ('dart', 'pub', 'get'), env=dart_env)
|
||||||
|
|
||||||
|
for executable in pubspec_contents['executables']:
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix_p,
|
||||||
|
(
|
||||||
|
'dart', 'compile', 'exe',
|
||||||
|
'--output', os.path.join(bin_dir, win_exe(executable)),
|
||||||
|
prefix_p.path('bin', f'{executable}.dart'),
|
||||||
|
),
|
||||||
|
env=dart_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
os.makedirs(bin_dir)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
_install_dir(prefix, tmp)
|
||||||
|
|
||||||
|
for dep_s in additional_dependencies:
|
||||||
|
with tempfile.TemporaryDirectory() as dep_tmp:
|
||||||
|
dep, _, version = dep_s.partition(':')
|
||||||
|
if version:
|
||||||
|
dep_cmd: tuple[str, ...] = (dep, '--version', version)
|
||||||
|
else:
|
||||||
|
dep_cmd = (dep,)
|
||||||
|
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix,
|
||||||
|
('dart', 'pub', 'cache', 'add', *dep_cmd),
|
||||||
|
env={**os.environ, 'PUB_CACHE': dep_tmp},
|
||||||
|
)
|
||||||
|
|
||||||
|
# try and find the 'pubspec.yaml' that just got added
|
||||||
|
for root, _, filenames in os.walk(dep_tmp):
|
||||||
|
if 'pubspec.yaml' in filenames:
|
||||||
|
with tempfile.TemporaryDirectory() as copied:
|
||||||
|
pkg = os.path.join(copied, 'pkg')
|
||||||
|
shutil.copytree(root, pkg)
|
||||||
|
_install_dir(Prefix(pkg), dep_tmp)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError(
|
||||||
|
f'could not find pubspec.yaml for {dep_s}',
|
||||||
|
)
|
||||||
|
|
@ -1,49 +1,86 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from pre_commit import five
|
from pre_commit import lang_base
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import CalledProcessError
|
from pre_commit.util import CalledProcessError
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.util import cmd_output_b
|
||||||
from pre_commit.util import cmd_output
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'docker'
|
ENVIRONMENT_DIR = 'docker'
|
||||||
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
||||||
get_default_version = helpers.basic_get_default_version
|
get_default_version = lang_base.basic_get_default_version
|
||||||
healthy = helpers.basic_healthy
|
health_check = lang_base.basic_health_check
|
||||||
|
in_env = lang_base.no_env # no special environment for docker
|
||||||
|
|
||||||
|
_HOSTNAME_MOUNT_RE = re.compile(
|
||||||
|
rb"""
|
||||||
|
/containers
|
||||||
|
(?:/overlay-containers)?
|
||||||
|
/([a-z0-9]{64})
|
||||||
|
(?:/userdata)?
|
||||||
|
/hostname
|
||||||
|
""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def md5(s): # pragma: windows no cover
|
def _get_container_id() -> str | None:
|
||||||
return hashlib.md5(five.to_bytes(s)).hexdigest()
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
with open('/proc/1/mountinfo', 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
m = _HOSTNAME_MOUNT_RE.search(line)
|
||||||
|
if m:
|
||||||
|
return m[1].decode()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def docker_tag(prefix): # pragma: windows no cover
|
def _get_docker_path(path: str) -> str:
|
||||||
md5sum = md5(os.path.basename(prefix.prefix_dir)).lower()
|
container_id = _get_container_id()
|
||||||
return 'pre-commit-{}'.format(md5sum)
|
if container_id is None:
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def docker_is_running(): # pragma: windows no cover
|
|
||||||
try:
|
try:
|
||||||
return cmd_output('docker', 'ps')[0] == 0
|
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
return False
|
# self-container was not visible from here (perhaps docker-in-docker)
|
||||||
|
return path
|
||||||
|
|
||||||
|
container, = json.loads(out)
|
||||||
|
for mount in container['Mounts']:
|
||||||
|
src_path = mount['Source']
|
||||||
|
to_path = mount['Destination']
|
||||||
|
if os.path.commonpath((path, to_path)) == to_path:
|
||||||
|
# So there is something in common,
|
||||||
|
# and we can proceed remapping it
|
||||||
|
return path.replace(to_path, src_path)
|
||||||
|
# we're in Docker, but the path is not mounted, cannot really do anything,
|
||||||
|
# so fall back to original path
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def assert_docker_available(): # pragma: windows no cover
|
def md5(s: str) -> str: # pragma: win32 no cover
|
||||||
assert docker_is_running(), (
|
return hashlib.md5(s.encode()).hexdigest()
|
||||||
'Docker is either not running or not configured in this environment'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_docker_image(prefix, **kwargs): # pragma: windows no cover
|
def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
|
||||||
pull = kwargs.pop('pull')
|
md5sum = md5(os.path.basename(prefix.prefix_dir)).lower()
|
||||||
assert not kwargs, kwargs
|
return f'pre-commit-{md5sum}'
|
||||||
cmd = (
|
|
||||||
|
|
||||||
|
def build_docker_image(
|
||||||
|
prefix: Prefix,
|
||||||
|
*,
|
||||||
|
pull: bool,
|
||||||
|
) -> None: # pragma: win32 no cover
|
||||||
|
cmd: tuple[str, ...] = (
|
||||||
'docker', 'build',
|
'docker', 'build',
|
||||||
'--tag', docker_tag(prefix),
|
'--tag', docker_tag(prefix),
|
||||||
'--label', PRE_COMMIT_LABEL,
|
'--label', PRE_COMMIT_LABEL,
|
||||||
|
|
@ -52,49 +89,93 @@ def build_docker_image(prefix, **kwargs): # pragma: windows no cover
|
||||||
cmd += ('--pull',)
|
cmd += ('--pull',)
|
||||||
# This must come last for old versions of docker. See #477
|
# This must come last for old versions of docker. See #477
|
||||||
cmd += ('.',)
|
cmd += ('.',)
|
||||||
helpers.run_setup_cmd(prefix, cmd)
|
lang_base.setup_cmd(prefix, cmd)
|
||||||
|
|
||||||
|
|
||||||
def install_environment(
|
def install_environment(
|
||||||
prefix, version, additional_dependencies,
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||||
): # pragma: windows no cover
|
) -> None: # pragma: win32 no cover
|
||||||
helpers.assert_version_default('docker', version)
|
lang_base.assert_version_default('docker', version)
|
||||||
helpers.assert_no_additional_deps('docker', additional_dependencies)
|
lang_base.assert_no_additional_deps('docker', additional_dependencies)
|
||||||
assert_docker_available()
|
|
||||||
|
|
||||||
directory = prefix.path(
|
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Docker doesn't really have relevant disk environment, but pre-commit
|
# Docker doesn't really have relevant disk environment, but pre-commit
|
||||||
# still needs to cleanup it's state files on failure
|
# still needs to cleanup its state files on failure
|
||||||
with clean_path_on_failure(directory):
|
build_docker_image(prefix, pull=True)
|
||||||
build_docker_image(prefix, pull=True)
|
os.mkdir(directory)
|
||||||
os.mkdir(directory)
|
|
||||||
|
|
||||||
|
|
||||||
def docker_cmd():
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def _is_rootless() -> bool: # pragma: win32 no cover
|
||||||
|
retcode, out, _ = cmd_output_b(
|
||||||
|
'docker', 'system', 'info', '--format', '{{ json . }}',
|
||||||
|
)
|
||||||
|
if retcode != 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
info = json.loads(out)
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
# docker:
|
||||||
|
# https://docs.docker.com/reference/api/engine/version/v1.48/#tag/System/operation/SystemInfo
|
||||||
|
'name=rootless' in (info.get('SecurityOptions') or ()) or
|
||||||
|
# podman:
|
||||||
|
# https://docs.podman.io/en/latest/_static/api.html?version=v5.4#tag/system/operation/SystemInfoLibpod
|
||||||
|
info['host']['security']['rootless']
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_docker_user() -> tuple[str, ...]: # pragma: win32 no cover
|
||||||
|
if _is_rootless():
|
||||||
|
return ()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ('-u', f'{os.getuid()}:{os.getgid()}')
|
||||||
|
except AttributeError:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
def get_docker_tty(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover # noqa: E501
|
||||||
|
return (('--tty',) if color else ())
|
||||||
|
|
||||||
|
|
||||||
|
def docker_cmd(*, color: bool) -> tuple[str, ...]: # pragma: win32 no cover
|
||||||
return (
|
return (
|
||||||
'docker', 'run',
|
'docker', 'run',
|
||||||
'--rm',
|
'--rm',
|
||||||
'-u', '{}:{}'.format(os.getuid(), os.getgid()),
|
*get_docker_tty(color=color),
|
||||||
|
*get_docker_user(),
|
||||||
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
|
# https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from
|
||||||
# The `Z` option tells Docker to label the content with a private
|
# The `Z` option tells Docker to label the content with a private
|
||||||
# unshared label. Only the current container can use a private volume.
|
# unshared label. Only the current container can use a private volume.
|
||||||
'-v', '{}:/src:rw,Z'.format(os.getcwd()),
|
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
|
||||||
'--workdir', '/src',
|
'--workdir', '/src',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args): # pragma: windows no cover
|
def run_hook(
|
||||||
assert_docker_available()
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]: # pragma: win32 no cover
|
||||||
# Rebuild the docker image in case it has gone missing, as many people do
|
# Rebuild the docker image in case it has gone missing, as many people do
|
||||||
# automated cleanup of docker images.
|
# automated cleanup of docker images.
|
||||||
build_docker_image(prefix, pull=False)
|
build_docker_image(prefix, pull=False)
|
||||||
|
|
||||||
hook_cmd = helpers.to_cmd(hook)
|
entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args)
|
||||||
entry_exe, cmd_rest = hook_cmd[0], hook_cmd[1:]
|
|
||||||
|
|
||||||
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
|
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
|
||||||
cmd = docker_cmd() + entry_tag + cmd_rest
|
return lang_base.run_xargs(
|
||||||
return xargs(cmd, file_args)
|
(*docker_cmd(color=color), *entry_tag, *cmd_rest),
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,32 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from pre_commit.languages import helpers
|
from collections.abc import Sequence
|
||||||
from pre_commit.languages.docker import assert_docker_available
|
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.languages.docker import docker_cmd
|
from pre_commit.languages.docker import docker_cmd
|
||||||
from pre_commit.xargs import xargs
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = None
|
ENVIRONMENT_DIR = None
|
||||||
get_default_version = helpers.basic_get_default_version
|
get_default_version = lang_base.basic_get_default_version
|
||||||
healthy = helpers.basic_healthy
|
health_check = lang_base.basic_health_check
|
||||||
install_environment = helpers.no_install
|
install_environment = lang_base.no_install
|
||||||
|
in_env = lang_base.no_env
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args): # pragma: windows no cover
|
def run_hook(
|
||||||
assert_docker_available()
|
prefix: Prefix,
|
||||||
cmd = docker_cmd() + helpers.to_cmd(hook)
|
entry: str,
|
||||||
return xargs(cmd, file_args)
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]: # pragma: win32 no cover
|
||||||
|
cmd = docker_cmd(color=color) + lang_base.hook_cmd(entry, args)
|
||||||
|
return lang_base.run_xargs(
|
||||||
|
cmd,
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
|
||||||
111
pre_commit/languages/dotnet.py
Normal file
111
pre_commit/languages/dotnet.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
import xml.etree.ElementTree
|
||||||
|
import zipfile
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'dotnetenv'
|
||||||
|
BIN_DIR = 'bin'
|
||||||
|
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('PATH', (os.path.join(venv, BIN_DIR), os.pathsep, Var('PATH'))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _nuget_config_no_sources() -> Generator[str]:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
nuget_config = os.path.join(tmpdir, 'nuget.config')
|
||||||
|
with open(nuget_config, 'w') as f:
|
||||||
|
f.write(
|
||||||
|
'<?xml version="1.0" encoding="utf-8"?>'
|
||||||
|
'<configuration>'
|
||||||
|
' <packageSources>'
|
||||||
|
' <clear />'
|
||||||
|
' </packageSources>'
|
||||||
|
'</configuration>',
|
||||||
|
)
|
||||||
|
yield nuget_config
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('dotnet', version)
|
||||||
|
lang_base.assert_no_additional_deps('dotnet', additional_dependencies)
|
||||||
|
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
build_dir = prefix.path('pre-commit-build')
|
||||||
|
|
||||||
|
# Build & pack nupkg file
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix,
|
||||||
|
(
|
||||||
|
'dotnet', 'pack',
|
||||||
|
'--configuration', 'Release',
|
||||||
|
'--property', f'PackageOutputPath={build_dir}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
nupkg_dir = prefix.path(build_dir)
|
||||||
|
nupkgs = [x for x in os.listdir(nupkg_dir) if x.endswith('.nupkg')]
|
||||||
|
|
||||||
|
if not nupkgs:
|
||||||
|
raise AssertionError('could not find any build outputs to install')
|
||||||
|
|
||||||
|
for nupkg in nupkgs:
|
||||||
|
with zipfile.ZipFile(os.path.join(nupkg_dir, nupkg)) as f:
|
||||||
|
nuspec, = (x for x in f.namelist() if x.endswith('.nuspec'))
|
||||||
|
with f.open(nuspec) as spec:
|
||||||
|
tree = xml.etree.ElementTree.parse(spec)
|
||||||
|
|
||||||
|
namespace = re.match(r'{.*}', tree.getroot().tag)
|
||||||
|
if not namespace:
|
||||||
|
raise AssertionError('could not parse namespace from nuspec')
|
||||||
|
|
||||||
|
tool_id_element = tree.find(f'.//{namespace[0]}id')
|
||||||
|
if tool_id_element is None:
|
||||||
|
raise AssertionError('expected to find an "id" element')
|
||||||
|
|
||||||
|
tool_id = tool_id_element.text
|
||||||
|
if not tool_id:
|
||||||
|
raise AssertionError('"id" element missing tool name')
|
||||||
|
|
||||||
|
# Install to bin dir
|
||||||
|
with _nuget_config_no_sources() as nuget_config:
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix,
|
||||||
|
(
|
||||||
|
'dotnet', 'tool', 'install',
|
||||||
|
'--configfile', nuget_config,
|
||||||
|
'--tool-path', os.path.join(envdir, BIN_DIR),
|
||||||
|
'--add-source', build_dir,
|
||||||
|
tool_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
27
pre_commit/languages/fail.py
Normal file
27
pre_commit/languages/fail.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = None
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
install_environment = lang_base.no_install
|
||||||
|
in_env = lang_base.no_env
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
out = f'{entry}\n\n'.encode()
|
||||||
|
out += b'\n'.join(f.encode() for f in file_args) + b'\n'
|
||||||
|
return 1, out
|
||||||
|
|
@ -1,84 +1,161 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import ContextManager
|
||||||
|
from typing import IO
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
from pre_commit import git
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.git import no_git_env
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
from pre_commit.util import rmtree
|
from pre_commit.util import rmtree
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'golangenv'
|
ENVIRONMENT_DIR = 'golangenv'
|
||||||
get_default_version = helpers.basic_get_default_version
|
health_check = lang_base.basic_health_check
|
||||||
healthy = helpers.basic_healthy
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
_ARCH_ALIASES = {
|
||||||
|
'x86_64': 'amd64',
|
||||||
|
'i386': '386',
|
||||||
|
'aarch64': 'arm64',
|
||||||
|
'armv8': 'arm64',
|
||||||
|
'armv7l': 'armv6l',
|
||||||
|
}
|
||||||
|
_ARCH = platform.machine().lower()
|
||||||
|
_ARCH = _ARCH_ALIASES.get(_ARCH, _ARCH)
|
||||||
|
|
||||||
|
|
||||||
def get_env_patch(venv):
|
class ExtractAll(Protocol):
|
||||||
|
def extractall(self, path: str) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == 'win32': # pragma: win32 cover
|
||||||
|
_EXT = 'zip'
|
||||||
|
|
||||||
|
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
|
||||||
|
return zipfile.ZipFile(bio)
|
||||||
|
else: # pragma: win32 no cover
|
||||||
|
_EXT = 'tar.gz'
|
||||||
|
|
||||||
|
def _open_archive(bio: IO[bytes]) -> ContextManager[ExtractAll]:
|
||||||
|
return tarfile.open(fileobj=bio)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def get_default_version() -> str:
|
||||||
|
if lang_base.exe_exists('go'):
|
||||||
|
return 'system'
|
||||||
|
else:
|
||||||
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(venv: str, version: str) -> PatchesT:
|
||||||
|
if version == 'system':
|
||||||
|
return (
|
||||||
|
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
|
('GOROOT', os.path.join(venv, '.go')),
|
||||||
|
('GOTOOLCHAIN', 'local'),
|
||||||
|
(
|
||||||
|
'PATH', (
|
||||||
|
os.path.join(venv, 'bin'), os.pathsep,
|
||||||
|
os.path.join(venv, '.go', 'bin'), os.pathsep, Var('PATH'),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache
|
||||||
|
def _infer_go_version(version: str) -> str:
|
||||||
|
if version != C.DEFAULT:
|
||||||
|
return version
|
||||||
|
resp = urllib.request.urlopen('https://go.dev/dl/?mode=json')
|
||||||
|
return json.load(resp)[0]['version'].removeprefix('go')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_url(version: str) -> str:
|
||||||
|
os_name = platform.system().lower()
|
||||||
|
version = _infer_go_version(version)
|
||||||
|
return f'https://dl.google.com/go/go{version}.{os_name}-{_ARCH}.{_EXT}'
|
||||||
|
|
||||||
|
|
||||||
|
def _install_go(version: str, dest: str) -> None:
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(_get_url(version))
|
||||||
|
except urllib.error.HTTPError as e: # pragma: no cover
|
||||||
|
if e.code == 404:
|
||||||
|
raise ValueError(
|
||||||
|
f'Could not find a version matching your system requirements '
|
||||||
|
f'(os={platform.system().lower()}; arch={_ARCH})',
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
with tempfile.TemporaryFile() as f:
|
||||||
|
shutil.copyfileobj(resp, f)
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
with _open_archive(f) as archive:
|
||||||
|
archive.extractall(dest)
|
||||||
|
shutil.move(os.path.join(dest, 'go'), os.path.join(dest, '.go'))
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def in_env(prefix):
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
envdir = prefix.path(
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
with envcontext(get_env_patch(envdir, version)):
|
||||||
)
|
|
||||||
with envcontext(get_env_patch(envdir)):
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def guess_go_dir(remote_url):
|
def install_environment(
|
||||||
if remote_url.endswith('.git'):
|
prefix: Prefix,
|
||||||
remote_url = remote_url[:-1 * len('.git')]
|
version: str,
|
||||||
looks_like_url = (
|
additional_dependencies: Sequence[str],
|
||||||
not remote_url.startswith('file://') and
|
) -> None:
|
||||||
('//' in remote_url or '@' in remote_url)
|
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
)
|
|
||||||
remote_url = remote_url.replace(':', '/')
|
if version != 'system':
|
||||||
if looks_like_url:
|
_install_go(version, env_dir)
|
||||||
_, _, remote_url = remote_url.rpartition('//')
|
|
||||||
_, _, remote_url = remote_url.rpartition('@')
|
if sys.platform == 'cygwin': # pragma: no cover
|
||||||
return remote_url
|
gopath = cmd_output('cygpath', '-w', env_dir)[1].strip()
|
||||||
else:
|
else:
|
||||||
return 'unknown_src_dir'
|
gopath = env_dir
|
||||||
|
|
||||||
|
env = no_git_env(dict(os.environ, GOPATH=gopath))
|
||||||
|
env.pop('GOBIN', None)
|
||||||
|
if version != 'system':
|
||||||
|
env['GOTOOLCHAIN'] = 'local'
|
||||||
|
env['GOROOT'] = os.path.join(env_dir, '.go')
|
||||||
|
env['PATH'] = os.pathsep.join((
|
||||||
|
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
|
||||||
|
))
|
||||||
|
|
||||||
def install_environment(prefix, version, additional_dependencies):
|
lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env)
|
||||||
helpers.assert_version_default('golang', version)
|
for dependency in additional_dependencies:
|
||||||
directory = prefix.path(
|
lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
|
||||||
)
|
|
||||||
|
|
||||||
with clean_path_on_failure(directory):
|
# save some disk space -- we don't need this after installation
|
||||||
remote = git.get_remote_url(prefix.prefix_dir)
|
pkgdir = os.path.join(env_dir, 'pkg')
|
||||||
repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
|
if os.path.exists(pkgdir): # pragma: no branch (always true on windows?)
|
||||||
|
rmtree(pkgdir)
|
||||||
# Clone into the goenv we'll create
|
|
||||||
helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir))
|
|
||||||
|
|
||||||
if sys.platform == 'cygwin': # pragma: no cover
|
|
||||||
_, gopath, _ = cmd_output('cygpath', '-w', directory)
|
|
||||||
gopath = gopath.strip()
|
|
||||||
else:
|
|
||||||
gopath = directory
|
|
||||||
env = dict(os.environ, GOPATH=gopath)
|
|
||||||
cmd_output('go', 'get', './...', cwd=repo_src_dir, env=env)
|
|
||||||
for dependency in additional_dependencies:
|
|
||||||
cmd_output('go', 'get', dependency, cwd=repo_src_dir, env=env)
|
|
||||||
# Same some disk space, we don't need these after installation
|
|
||||||
rmtree(prefix.path(directory, 'src'))
|
|
||||||
pkgdir = prefix.path(directory, 'pkg')
|
|
||||||
if os.path.exists(pkgdir): # pragma: no cover (go<1.10)
|
|
||||||
rmtree(pkgdir)
|
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
with in_env(prefix):
|
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
|
|
|
||||||
56
pre_commit/languages/haskell.py
Normal file
56
pre_commit/languages/haskell.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os.path
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.errors import FatalError
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'hs_env'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(target_dir: str) -> PatchesT:
|
||||||
|
bin_path = os.path.join(target_dir, 'bin')
|
||||||
|
return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('haskell', version)
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
|
pkgs = [*prefix.star('.cabal'), *additional_dependencies]
|
||||||
|
if not pkgs:
|
||||||
|
raise FatalError('Expected .cabal files or additional_dependencies')
|
||||||
|
|
||||||
|
bindir = os.path.join(envdir, 'bin')
|
||||||
|
os.makedirs(bindir, exist_ok=True)
|
||||||
|
lang_base.setup_cmd(prefix, ('cabal', 'update'))
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix,
|
||||||
|
(
|
||||||
|
'cabal', 'install',
|
||||||
|
'--install-method', 'copy',
|
||||||
|
'--installdir', bindir,
|
||||||
|
*pkgs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
from pre_commit.util import cmd_output
|
|
||||||
|
|
||||||
|
|
||||||
def run_setup_cmd(prefix, cmd):
|
|
||||||
cmd_output(*cmd, cwd=prefix.prefix_dir, encoding=None)
|
|
||||||
|
|
||||||
|
|
||||||
def environment_dir(ENVIRONMENT_DIR, language_version):
|
|
||||||
if ENVIRONMENT_DIR is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return '{}-{}'.format(ENVIRONMENT_DIR, language_version)
|
|
||||||
|
|
||||||
|
|
||||||
def to_cmd(hook):
|
|
||||||
return tuple(shlex.split(hook['entry'])) + tuple(hook['args'])
|
|
||||||
|
|
||||||
|
|
||||||
def assert_version_default(binary, version):
|
|
||||||
if version != 'default':
|
|
||||||
raise AssertionError(
|
|
||||||
'For now, pre-commit requires system-installed {}'.format(binary),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_no_additional_deps(lang, additional_deps):
|
|
||||||
if additional_deps:
|
|
||||||
raise AssertionError(
|
|
||||||
'For now, pre-commit does not support '
|
|
||||||
'additional_dependencies for {}'.format(lang),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def basic_get_default_version():
|
|
||||||
return 'default'
|
|
||||||
|
|
||||||
|
|
||||||
def basic_healthy(prefix, language_version):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def no_install(prefix, version, additional_dependencies):
|
|
||||||
raise AssertionError('This type is not installable')
|
|
||||||
133
pre_commit/languages/julia.py
Normal file
133
pre_commit/languages/julia.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import UNSET
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import cmd_output_b
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'juliaenv'
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
# `entry` is a (hook-repo relative) file followed by (optional) args, e.g.
|
||||||
|
# `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we
|
||||||
|
# 1) shell parse it and join with args with hook_cmd
|
||||||
|
# 2) prepend the hooks prefix path to the first argument (the file), unless
|
||||||
|
# it is a local script
|
||||||
|
# 3) prepend `julia` as the interpreter
|
||||||
|
|
||||||
|
cmd = lang_base.hook_cmd(entry, args)
|
||||||
|
script = cmd[0] if is_local else prefix.path(cmd[0])
|
||||||
|
cmd = ('julia', '--startup-file=no', script, *cmd[1:])
|
||||||
|
return lang_base.run_xargs(
|
||||||
|
cmd,
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(target_dir: str, version: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('JULIA_LOAD_PATH', target_dir),
|
||||||
|
# May be set, remove it to not interfer with LOAD_PATH
|
||||||
|
('JULIA_PROJECT', UNSET),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir, version)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with in_env(prefix, version):
|
||||||
|
# TODO: Support language_version with juliaup similar to rust via
|
||||||
|
# rustup
|
||||||
|
# if version != 'system':
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Copy Project.toml to hook env if it exist
|
||||||
|
os.makedirs(envdir, exist_ok=True)
|
||||||
|
project_names = ('JuliaProject.toml', 'Project.toml')
|
||||||
|
project_found = False
|
||||||
|
for project_name in project_names:
|
||||||
|
project_file = prefix.path(project_name)
|
||||||
|
if not os.path.isfile(project_file):
|
||||||
|
continue
|
||||||
|
shutil.copy(project_file, envdir)
|
||||||
|
project_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no project file was found we create an empty one so that the
|
||||||
|
# package manager doesn't error
|
||||||
|
if not project_found:
|
||||||
|
open(os.path.join(envdir, 'Project.toml'), 'a').close()
|
||||||
|
|
||||||
|
# Copy Manifest.toml to hook env if it exists
|
||||||
|
manifest_names = ('JuliaManifest.toml', 'Manifest.toml')
|
||||||
|
for manifest_name in manifest_names:
|
||||||
|
manifest_file = prefix.path(manifest_name)
|
||||||
|
if not os.path.isfile(manifest_file):
|
||||||
|
continue
|
||||||
|
shutil.copy(manifest_file, envdir)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Julia code to instantiate the hook environment
|
||||||
|
julia_code = """
|
||||||
|
@assert length(ARGS) > 0
|
||||||
|
hook_env = ARGS[1]
|
||||||
|
deps = join(ARGS[2:end], " ")
|
||||||
|
|
||||||
|
# We prepend @stdlib here so that we can load the package manager even
|
||||||
|
# though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env.
|
||||||
|
pushfirst!(LOAD_PATH, "@stdlib")
|
||||||
|
using Pkg
|
||||||
|
popfirst!(LOAD_PATH)
|
||||||
|
|
||||||
|
# Instantiate the environment shipped with the hook repo. If we have
|
||||||
|
# additional dependencies we disable precompilation in this step to
|
||||||
|
# avoid double work.
|
||||||
|
precompile = isempty(deps) ? "1" : "0"
|
||||||
|
withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do
|
||||||
|
Pkg.instantiate()
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add additional dependencies (with precompilation)
|
||||||
|
if !isempty(deps)
|
||||||
|
withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do
|
||||||
|
Pkg.REPLMode.pkgstr("add " * deps)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
"""
|
||||||
|
cmd_output_b(
|
||||||
|
'julia', '--startup-file=no', '-e', julia_code, '--', envdir,
|
||||||
|
*additional_dependencies,
|
||||||
|
cwd=prefix.prefix_dir,
|
||||||
|
)
|
||||||
75
pre_commit/languages/lua.py
Normal file
75
pre_commit/languages/lua.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import cmd_output
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'lua_env'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lua_version() -> str: # pragma: win32 no cover
|
||||||
|
"""Get the Lua version used in file paths."""
|
||||||
|
_, stdout, _ = cmd_output('luarocks', 'config', '--lua-ver')
|
||||||
|
return stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(d: str) -> PatchesT: # pragma: win32 no cover
|
||||||
|
version = _get_lua_version()
|
||||||
|
so_ext = 'dll' if sys.platform == 'win32' else 'so'
|
||||||
|
return (
|
||||||
|
('PATH', (os.path.join(d, 'bin'), os.pathsep, Var('PATH'))),
|
||||||
|
(
|
||||||
|
'LUA_PATH', (
|
||||||
|
os.path.join(d, 'share', 'lua', version, '?.lua;'),
|
||||||
|
os.path.join(d, 'share', 'lua', version, '?', 'init.lua;;'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'LUA_CPATH',
|
||||||
|
(os.path.join(d, 'lib', 'lua', version, f'?.{so_ext};;'),),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager # pragma: win32 no cover
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None: # pragma: win32 no cover
|
||||||
|
lang_base.assert_version_default('lua', version)
|
||||||
|
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with in_env(prefix, version):
|
||||||
|
# luarocks doesn't bootstrap a tree prior to installing
|
||||||
|
# so ensure the directory exists.
|
||||||
|
os.makedirs(envdir, exist_ok=True)
|
||||||
|
|
||||||
|
# Older luarocks (e.g., 2.4.2) expect the rockspec as an arg
|
||||||
|
for rockspec in prefix.star('.rockspec'):
|
||||||
|
make_cmd = ('luarocks', '--tree', envdir, 'make', rockspec)
|
||||||
|
lang_base.setup_cmd(prefix, make_cmd)
|
||||||
|
|
||||||
|
# luarocks can't install multiple packages at once
|
||||||
|
# so install them individually.
|
||||||
|
for dependency in additional_dependencies:
|
||||||
|
cmd = ('luarocks', '--tree', envdir, 'install', dependency)
|
||||||
|
lang_base.setup_cmd(prefix, cmd)
|
||||||
|
|
@ -1,74 +1,110 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import UNSET
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
|
||||||
from pre_commit.languages.python import bin_dir
|
from pre_commit.languages.python import bin_dir
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
from pre_commit.xargs import xargs
|
from pre_commit.util import cmd_output_b
|
||||||
|
from pre_commit.util import rmtree
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'node_env'
|
ENVIRONMENT_DIR = 'node_env'
|
||||||
get_default_version = helpers.basic_get_default_version
|
run_hook = lang_base.basic_run_hook
|
||||||
healthy = helpers.basic_healthy
|
|
||||||
|
|
||||||
|
|
||||||
def _envdir(prefix, version):
|
@functools.lru_cache(maxsize=1)
|
||||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
def get_default_version() -> str:
|
||||||
return prefix.path(directory)
|
# nodeenv does not yet support `-n system` on windows
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
return C.DEFAULT
|
||||||
|
# if node is already installed, we can save a bunch of setup time by
|
||||||
|
# using the installed version
|
||||||
|
elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')):
|
||||||
|
return 'system'
|
||||||
|
else:
|
||||||
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
def get_env_patch(venv):
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
if sys.platform == 'cygwin': # pragma: no cover
|
if sys.platform == 'cygwin': # pragma: no cover
|
||||||
_, win_venv, _ = cmd_output('cygpath', '-w', venv)
|
_, win_venv, _ = cmd_output('cygpath', '-w', venv)
|
||||||
install_prefix = r'{}\bin'.format(win_venv.strip())
|
install_prefix = fr'{win_venv.strip()}\bin'
|
||||||
|
lib_dir = 'lib'
|
||||||
elif sys.platform == 'win32': # pragma: no cover
|
elif sys.platform == 'win32': # pragma: no cover
|
||||||
install_prefix = bin_dir(venv)
|
install_prefix = bin_dir(venv)
|
||||||
else:
|
lib_dir = 'Scripts'
|
||||||
|
else: # pragma: win32 no cover
|
||||||
install_prefix = venv
|
install_prefix = venv
|
||||||
|
lib_dir = 'lib'
|
||||||
return (
|
return (
|
||||||
('NODE_VIRTUAL_ENV', venv),
|
('NODE_VIRTUAL_ENV', venv),
|
||||||
('NPM_CONFIG_PREFIX', install_prefix),
|
('NPM_CONFIG_PREFIX', install_prefix),
|
||||||
('npm_config_prefix', install_prefix),
|
('npm_config_prefix', install_prefix),
|
||||||
('NODE_PATH', os.path.join(venv, 'lib', 'node_modules')),
|
('NPM_CONFIG_USERCONFIG', UNSET),
|
||||||
|
('npm_config_userconfig', UNSET),
|
||||||
|
('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')),
|
||||||
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def in_env(prefix, language_version):
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
with envcontext(get_env_patch(_envdir(prefix, language_version))):
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def install_environment(prefix, version, additional_dependencies):
|
def health_check(prefix: Prefix, version: str) -> str | None:
|
||||||
additional_dependencies = tuple(additional_dependencies)
|
with in_env(prefix, version):
|
||||||
|
retcode, _, _ = cmd_output_b('node', '--version', check=False)
|
||||||
|
if retcode != 0: # pragma: win32 no cover
|
||||||
|
return f'`node --version` returned {retcode}'
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
assert prefix.exists('package.json')
|
assert prefix.exists('package.json')
|
||||||
envdir = _envdir(prefix, version)
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath
|
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath
|
||||||
if sys.platform == 'win32': # pragma: no cover
|
if sys.platform == 'win32': # pragma: no cover
|
||||||
envdir = '\\\\?\\' + os.path.normpath(envdir)
|
envdir = fr'\\?\{os.path.normpath(envdir)}'
|
||||||
with clean_path_on_failure(envdir):
|
cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir]
|
||||||
cmd = [
|
if version != C.DEFAULT:
|
||||||
sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
|
cmd.extend(['-n', version])
|
||||||
]
|
cmd_output_b(*cmd)
|
||||||
if version != 'default':
|
|
||||||
cmd.extend(['-n', version])
|
|
||||||
cmd_output(*cmd)
|
|
||||||
|
|
||||||
with in_env(prefix, version):
|
with in_env(prefix, version):
|
||||||
helpers.run_setup_cmd(
|
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
|
||||||
prefix,
|
# install as if we installed from git
|
||||||
('npm', 'install', '-g', '.') + additional_dependencies,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
local_install_cmd = (
|
||||||
|
'npm', 'install', '--include=dev', '--include=prod',
|
||||||
|
'--ignore-prepublish', '--no-progress', '--no-save',
|
||||||
|
)
|
||||||
|
lang_base.setup_cmd(prefix, local_install_cmd)
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
_, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir)
|
||||||
with in_env(prefix, hook['language_version']):
|
pkg = prefix.path(pkg.strip())
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
|
install = ('npm', 'install', '-g', pkg, *additional_dependencies)
|
||||||
|
lang_base.setup_cmd(prefix, install)
|
||||||
|
|
||||||
|
# clean these up after installation
|
||||||
|
if prefix.exists('node_modules'): # pragma: win32 no cover
|
||||||
|
rmtree(prefix.path('node_modules'))
|
||||||
|
os.remove(pkg)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pre_commit.languages import helpers
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = None
|
|
||||||
GREP = 'ggrep' if sys.platform == 'darwin' else 'grep'
|
|
||||||
get_default_version = helpers.basic_get_default_version
|
|
||||||
healthy = helpers.basic_healthy
|
|
||||||
install_environment = helpers.no_install
|
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
# For PCRE the entry is the regular expression to match
|
|
||||||
cmd = (GREP, '-H', '-n', '-P') + tuple(hook['args']) + (hook['entry'],)
|
|
||||||
|
|
||||||
# Grep usually returns 0 for matches, and nonzero for non-matches so we
|
|
||||||
# negate it here.
|
|
||||||
return xargs(cmd, file_args, negate=True)
|
|
||||||
50
pre_commit/languages/perl.py
Normal file
50
pre_commit/languages/perl.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import Var
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'perl_env'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))),
|
||||||
|
('PERL5LIB', os.path.join(venv, 'lib', 'perl5')),
|
||||||
|
('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'),
|
||||||
|
(
|
||||||
|
'PERL_MM_OPT', (
|
||||||
|
f'INSTALL_BASE={shlex.quote(venv)} '
|
||||||
|
f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('perl', version)
|
||||||
|
|
||||||
|
with in_env(prefix, version):
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix, ('cpan', '-T', '.', *additional_dependencies),
|
||||||
|
)
|
||||||
|
|
@ -1,33 +1,36 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from re import Pattern
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.xargs import xargs
|
from pre_commit.xargs import xargs
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = None
|
ENVIRONMENT_DIR = None
|
||||||
get_default_version = helpers.basic_get_default_version
|
get_default_version = lang_base.basic_get_default_version
|
||||||
healthy = helpers.basic_healthy
|
health_check = lang_base.basic_health_check
|
||||||
install_environment = helpers.no_install
|
install_environment = lang_base.no_install
|
||||||
|
in_env = lang_base.no_env
|
||||||
|
|
||||||
|
|
||||||
def _process_filename_by_line(pattern, filename):
|
def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
|
||||||
retv = 0
|
retv = 0
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
for line_no, line in enumerate(f, start=1):
|
for line_no, line in enumerate(f, start=1):
|
||||||
if pattern.search(line):
|
if pattern.search(line):
|
||||||
retv = 1
|
retv = 1
|
||||||
output.write('{}:{}:'.format(filename, line_no))
|
output.write(f'{filename}:{line_no}:')
|
||||||
output.write_line(line.rstrip(b'\r\n'))
|
output.write_line_b(line.rstrip(b'\r\n'))
|
||||||
return retv
|
return retv
|
||||||
|
|
||||||
|
|
||||||
def _process_filename_at_once(pattern, filename):
|
def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
|
||||||
retv = 0
|
retv = 0
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, 'rb') as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
|
|
@ -35,22 +38,70 @@ def _process_filename_at_once(pattern, filename):
|
||||||
if match:
|
if match:
|
||||||
retv = 1
|
retv = 1
|
||||||
line_no = contents[:match.start()].count(b'\n')
|
line_no = contents[:match.start()].count(b'\n')
|
||||||
output.write('{}:{}:'.format(filename, line_no + 1))
|
output.write(f'{filename}:{line_no + 1}:')
|
||||||
|
|
||||||
matched_lines = match.group().split(b'\n')
|
matched_lines = match[0].split(b'\n')
|
||||||
matched_lines[0] = contents.split(b'\n')[line_no]
|
matched_lines[0] = contents.split(b'\n')[line_no]
|
||||||
|
|
||||||
output.write_line(b'\n'.join(matched_lines))
|
output.write_line_b(b'\n'.join(matched_lines))
|
||||||
return retv
|
return retv
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
def _process_filename_by_line_negated(
|
||||||
exe = (sys.executable, '-m', __name__)
|
pattern: Pattern[bytes],
|
||||||
exe += tuple(hook['args']) + (hook['entry'],)
|
filename: str,
|
||||||
return xargs(exe, file_args)
|
) -> int:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
for line in f:
|
||||||
|
if pattern.search(line):
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
output.write_line(filename)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def _process_filename_at_once_negated(
|
||||||
|
pattern: Pattern[bytes],
|
||||||
|
filename: str,
|
||||||
|
) -> int:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
contents = f.read()
|
||||||
|
match = pattern.search(contents)
|
||||||
|
if match:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
output.write_line(filename)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class Choice(NamedTuple):
|
||||||
|
multiline: bool
|
||||||
|
negate: bool
|
||||||
|
|
||||||
|
|
||||||
|
FNS = {
|
||||||
|
Choice(multiline=True, negate=True): _process_filename_at_once_negated,
|
||||||
|
Choice(multiline=True, negate=False): _process_filename_at_once,
|
||||||
|
Choice(multiline=False, negate=True): _process_filename_by_line_negated,
|
||||||
|
Choice(multiline=False, negate=False): _process_filename_by_line,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
cmd = (sys.executable, '-m', __name__, *args, entry)
|
||||||
|
return xargs(cmd, file_args, color=color)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=(
|
description=(
|
||||||
'grep-like finder using python regexes. Unlike grep, this tool '
|
'grep-like finder using python regexes. Unlike grep, this tool '
|
||||||
|
|
@ -60,6 +111,7 @@ def main(argv=None):
|
||||||
)
|
)
|
||||||
parser.add_argument('-i', '--ignore-case', action='store_true')
|
parser.add_argument('-i', '--ignore-case', action='store_true')
|
||||||
parser.add_argument('--multiline', action='store_true')
|
parser.add_argument('--multiline', action='store_true')
|
||||||
|
parser.add_argument('--negate', action='store_true')
|
||||||
parser.add_argument('pattern', help='python regex pattern.')
|
parser.add_argument('pattern', help='python regex pattern.')
|
||||||
parser.add_argument('filenames', nargs='*')
|
parser.add_argument('filenames', nargs='*')
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
@ -71,13 +123,11 @@ def main(argv=None):
|
||||||
pattern = re.compile(args.pattern.encode(), flags)
|
pattern = re.compile(args.pattern.encode(), flags)
|
||||||
|
|
||||||
retv = 0
|
retv = 0
|
||||||
|
process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)]
|
||||||
for filename in args.filenames:
|
for filename in args.filenames:
|
||||||
if args.multiline:
|
retv |= process_fn(pattern, filename)
|
||||||
retv |= _process_filename_at_once(pattern, filename)
|
|
||||||
else:
|
|
||||||
retv |= _process_filename_by_line(pattern, filename)
|
|
||||||
return retv
|
return retv
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,95 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
from pre_commit.envcontext import UNSET
|
from pre_commit.envcontext import UNSET
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
|
||||||
from pre_commit.parse_shebang import find_executable
|
from pre_commit.parse_shebang import find_executable
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import CalledProcessError
|
from pre_commit.util import CalledProcessError
|
||||||
from pre_commit.util import clean_path_on_failure
|
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import cmd_output
|
||||||
from pre_commit.xargs import xargs
|
from pre_commit.util import cmd_output_b
|
||||||
|
from pre_commit.util import win_exe
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'py_env'
|
ENVIRONMENT_DIR = 'py_env'
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
def bin_dir(venv):
|
@functools.cache
|
||||||
|
def _version_info(exe: str) -> str:
|
||||||
|
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
|
||||||
|
try:
|
||||||
|
return cmd_output(exe, '-S', '-c', prog)[1].strip()
|
||||||
|
except CalledProcessError:
|
||||||
|
return f'<<error retrieving version from {exe}>>'
|
||||||
|
|
||||||
|
|
||||||
|
def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
|
||||||
|
ret = {}
|
||||||
|
with open(filename, encoding='UTF-8') as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
k, v = line.split('=')
|
||||||
|
except ValueError: # blank line / comment / etc.
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
ret[k.strip()] = v.strip()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def bin_dir(venv: str) -> str:
|
||||||
"""On windows there's a different directory for the virtualenv"""
|
"""On windows there's a different directory for the virtualenv"""
|
||||||
bin_part = 'Scripts' if os.name == 'nt' else 'bin'
|
bin_part = 'Scripts' if sys.platform == 'win32' else 'bin'
|
||||||
return os.path.join(venv, bin_part)
|
return os.path.join(venv, bin_part)
|
||||||
|
|
||||||
|
|
||||||
def get_env_patch(venv):
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
return (
|
return (
|
||||||
|
('PIP_DISABLE_PIP_VERSION_CHECK', '1'),
|
||||||
('PYTHONHOME', UNSET),
|
('PYTHONHOME', UNSET),
|
||||||
('VIRTUAL_ENV', venv),
|
('VIRTUAL_ENV', venv),
|
||||||
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _find_by_py_launcher(version): # pragma: no cover (windows only)
|
def _find_by_py_launcher(
|
||||||
|
version: str,
|
||||||
|
) -> str | None: # pragma: no cover (windows only)
|
||||||
if version.startswith('python'):
|
if version.startswith('python'):
|
||||||
|
num = version.removeprefix('python')
|
||||||
|
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
|
||||||
|
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
|
||||||
try:
|
try:
|
||||||
return cmd_output(
|
return cmd_output(*cmd, env=env)[1].strip()
|
||||||
'py', '-{}'.format(version[len('python'):]),
|
|
||||||
'-c', 'import sys; print(sys.executable)',
|
|
||||||
)[1].strip()
|
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
pass
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_default_version(): # pragma: no cover (platform dependent)
|
def _impl_exe_name() -> str:
|
||||||
def _norm(path):
|
if sys.implementation.name == 'cpython': # pragma: cpython cover
|
||||||
|
return 'python'
|
||||||
|
else: # pragma: cpython no cover
|
||||||
|
return sys.implementation.name # pypy mostly
|
||||||
|
|
||||||
|
|
||||||
|
def _find_by_sys_executable() -> str | None:
|
||||||
|
def _norm(path: str) -> str | None:
|
||||||
_, exe = os.path.split(path.lower())
|
_, exe = os.path.split(path.lower())
|
||||||
exe, _, _ = exe.partition('.exe')
|
exe, _, _ = exe.partition('.exe')
|
||||||
if find_executable(exe) and exe not in {'python', 'pythonw'}:
|
if exe not in {'python', 'pythonw'} and find_executable(exe):
|
||||||
return exe
|
return exe
|
||||||
|
return None
|
||||||
|
|
||||||
# First attempt from `sys.executable` (or the realpath)
|
|
||||||
# On linux, I see these common sys.executables:
|
# On linux, I see these common sys.executables:
|
||||||
#
|
#
|
||||||
# system `python`: /usr/bin/python -> python2.7
|
# system `python`: /usr/bin/python -> python2.7
|
||||||
|
|
@ -59,100 +98,131 @@ def _get_default_version(): # pragma: no cover (platform dependent)
|
||||||
# virtualenv v -ppython2: v/bin/python -> python2
|
# virtualenv v -ppython2: v/bin/python -> python2
|
||||||
# virtualenv v -ppython2.7: v/bin/python -> python2.7
|
# virtualenv v -ppython2.7: v/bin/python -> python2.7
|
||||||
# virtualenv v -ppypy: v/bin/python -> v/bin/pypy
|
# virtualenv v -ppypy: v/bin/python -> v/bin/pypy
|
||||||
for path in {sys.executable, os.path.realpath(sys.executable)}:
|
for path in (sys.executable, os.path.realpath(sys.executable)):
|
||||||
exe = _norm(path)
|
exe = _norm(path)
|
||||||
if exe:
|
if exe:
|
||||||
return exe
|
return exe
|
||||||
|
return None
|
||||||
|
|
||||||
# Next try the `pythonX.X` executable
|
|
||||||
exe = 'python{}.{}'.format(*sys.version_info)
|
|
||||||
if find_executable(exe):
|
|
||||||
return exe
|
|
||||||
|
|
||||||
if _find_by_py_launcher(exe):
|
@functools.lru_cache(maxsize=1)
|
||||||
return exe
|
def get_default_version() -> str: # pragma: no cover (platform dependent)
|
||||||
|
v_major = f'{sys.version_info[0]}'
|
||||||
|
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
|
||||||
|
|
||||||
# Give a best-effort try for windows
|
# attempt the likely implementation exe
|
||||||
if os.path.exists(r'C:\{}\python.exe'.format(exe.replace('.', ''))):
|
for potential in (v_minor, v_major):
|
||||||
return exe
|
exe = f'{_impl_exe_name()}{potential}'
|
||||||
|
if find_executable(exe):
|
||||||
|
return exe
|
||||||
|
|
||||||
|
# next try `sys.executable` (or the realpath)
|
||||||
|
maybe_exe = _find_by_sys_executable()
|
||||||
|
if maybe_exe:
|
||||||
|
return maybe_exe
|
||||||
|
|
||||||
|
# maybe on windows we can find it via py launcher?
|
||||||
|
if sys.platform == 'win32': # pragma: win32 cover
|
||||||
|
exe = f'python{v_minor}'
|
||||||
|
if _find_by_py_launcher(exe):
|
||||||
|
return exe
|
||||||
|
|
||||||
# We tried!
|
# We tried!
|
||||||
return 'default'
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
def get_default_version():
|
def _sys_executable_matches(version: str) -> bool:
|
||||||
# TODO: when dropping python2, use `functools.lru_cache(maxsize=1)`
|
if version == 'python':
|
||||||
|
return True
|
||||||
|
elif not version.startswith('python'):
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return get_default_version.cached_version
|
info = tuple(int(p) for p in version.removeprefix('python').split('.'))
|
||||||
except AttributeError:
|
except ValueError:
|
||||||
get_default_version.cached_version = _get_default_version()
|
return False
|
||||||
return get_default_version()
|
|
||||||
|
return sys.version_info[:len(info)] == info
|
||||||
|
|
||||||
|
|
||||||
def norm_version(version):
|
def norm_version(version: str) -> str | None:
|
||||||
if os.name == 'nt': # pragma: no cover (windows)
|
if version == C.DEFAULT: # use virtualenv's default
|
||||||
|
return None
|
||||||
|
elif _sys_executable_matches(version): # virtualenv defaults to our exe
|
||||||
|
return None
|
||||||
|
|
||||||
|
if sys.platform == 'win32': # pragma: no cover (windows)
|
||||||
|
version_exec = _find_by_py_launcher(version)
|
||||||
|
if version_exec:
|
||||||
|
return version_exec
|
||||||
|
|
||||||
# Try looking up by name
|
# Try looking up by name
|
||||||
version_exec = find_executable(version)
|
version_exec = find_executable(version)
|
||||||
if version_exec and version_exec != version:
|
if version_exec and version_exec != version:
|
||||||
return version_exec
|
return version_exec
|
||||||
|
|
||||||
version_exec = _find_by_py_launcher(version)
|
|
||||||
if version_exec:
|
|
||||||
return version_exec
|
|
||||||
|
|
||||||
# If it is in the form pythonx.x search in the default
|
|
||||||
# place on windows
|
|
||||||
if version.startswith('python'):
|
|
||||||
return r'C:\{}\python.exe'.format(version.replace('.', ''))
|
|
||||||
|
|
||||||
# Otherwise assume it is a path
|
# Otherwise assume it is a path
|
||||||
return os.path.expanduser(version)
|
return os.path.expanduser(version)
|
||||||
|
|
||||||
|
|
||||||
def py_interface(_dir, _make_venv):
|
@contextlib.contextmanager
|
||||||
@contextlib.contextmanager
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
def in_env(prefix, language_version):
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
envdir = prefix.path(helpers.environment_dir(_dir, language_version))
|
with envcontext(get_env_patch(envdir)):
|
||||||
with envcontext(get_env_patch(envdir)):
|
yield
|
||||||
yield
|
|
||||||
|
|
||||||
def healthy(prefix, language_version):
|
|
||||||
with in_env(prefix, language_version):
|
|
||||||
retcode, _, _ = cmd_output(
|
|
||||||
'python', '-c',
|
|
||||||
'import ctypes, datetime, io, os, ssl, weakref',
|
|
||||||
retcode=None,
|
|
||||||
)
|
|
||||||
return retcode == 0
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
with in_env(prefix, hook['language_version']):
|
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
|
|
||||||
def install_environment(prefix, version, additional_dependencies):
|
|
||||||
additional_dependencies = tuple(additional_dependencies)
|
|
||||||
directory = helpers.environment_dir(_dir, version)
|
|
||||||
|
|
||||||
env_dir = prefix.path(directory)
|
|
||||||
with clean_path_on_failure(env_dir):
|
|
||||||
if version != 'default':
|
|
||||||
python = norm_version(version)
|
|
||||||
else:
|
|
||||||
python = os.path.realpath(sys.executable)
|
|
||||||
_make_venv(env_dir, python)
|
|
||||||
with in_env(prefix, version):
|
|
||||||
helpers.run_setup_cmd(
|
|
||||||
prefix, ('pip', 'install', '.') + additional_dependencies,
|
|
||||||
)
|
|
||||||
|
|
||||||
return in_env, healthy, run_hook, install_environment
|
|
||||||
|
|
||||||
|
|
||||||
def make_venv(envdir, python):
|
def health_check(prefix: Prefix, version: str) -> str | None:
|
||||||
env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
|
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
|
||||||
cmd_output(*cmd, env=env, cwd='/')
|
|
||||||
|
# created with "old" virtualenv
|
||||||
|
if not os.path.exists(pyvenv_cfg):
|
||||||
|
return 'pyvenv.cfg does not exist (old virtualenv?)'
|
||||||
|
|
||||||
|
exe_name = win_exe('python')
|
||||||
|
py_exe = prefix.path(bin_dir(envdir), exe_name)
|
||||||
|
cfg = _read_pyvenv_cfg(pyvenv_cfg)
|
||||||
|
|
||||||
|
if 'version_info' not in cfg:
|
||||||
|
return "created virtualenv's pyvenv.cfg is missing `version_info`"
|
||||||
|
|
||||||
|
# always use uncached lookup here in case we replaced an unhealthy env
|
||||||
|
virtualenv_version = _version_info.__wrapped__(py_exe)
|
||||||
|
if virtualenv_version != cfg['version_info']:
|
||||||
|
return (
|
||||||
|
f'virtualenv python version did not match created version:\n'
|
||||||
|
f'- actual version: {virtualenv_version}\n'
|
||||||
|
f'- expected version: {cfg["version_info"]}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# made with an older version of virtualenv? skip `base-executable` check
|
||||||
|
if 'base-executable' not in cfg:
|
||||||
|
return None
|
||||||
|
|
||||||
|
base_exe_version = _version_info(cfg['base-executable'])
|
||||||
|
if base_exe_version != cfg['version_info']:
|
||||||
|
return (
|
||||||
|
f'base executable python version does not match created version:\n'
|
||||||
|
f'- base-executable version: {base_exe_version}\n'
|
||||||
|
f'- expected version: {cfg["version_info"]}\n'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
_interface = py_interface(ENVIRONMENT_DIR, make_venv)
|
def install_environment(
|
||||||
in_env, healthy, run_hook, install_environment = _interface
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
venv_cmd = [sys.executable, '-mvirtualenv', envdir]
|
||||||
|
python = norm_version(version)
|
||||||
|
if python is not None:
|
||||||
|
venv_cmd.extend(('-p', python))
|
||||||
|
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
|
||||||
|
|
||||||
|
cmd_output_b(*venv_cmd, cwd='/')
|
||||||
|
with in_env(prefix, version):
|
||||||
|
lang_base.setup_cmd(prefix, install_cmd)
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pre_commit.languages import python
|
|
||||||
from pre_commit.util import CalledProcessError
|
|
||||||
from pre_commit.util import cmd_output
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'py_venv'
|
|
||||||
|
|
||||||
|
|
||||||
def get_default_version(): # pragma: no cover (version specific)
|
|
||||||
if sys.version_info < (3,):
|
|
||||||
return 'python3'
|
|
||||||
else:
|
|
||||||
return python.get_default_version()
|
|
||||||
|
|
||||||
|
|
||||||
def orig_py_exe(exe): # pragma: no cover (platform specific)
|
|
||||||
"""A -mvenv virtualenv made from a -mvirtualenv virtualenv installs
|
|
||||||
packages to the incorrect location. Attempt to find the _original_ exe
|
|
||||||
and invoke `-mvenv` from there.
|
|
||||||
|
|
||||||
See:
|
|
||||||
- https://github.com/pre-commit/pre-commit/issues/755
|
|
||||||
- https://github.com/pypa/virtualenv/issues/1095
|
|
||||||
- https://bugs.python.org/issue30811
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
prefix_script = 'import sys; print(sys.real_prefix)'
|
|
||||||
_, prefix, _ = cmd_output(exe, '-c', prefix_script)
|
|
||||||
prefix = prefix.strip()
|
|
||||||
except CalledProcessError:
|
|
||||||
# not created from -mvirtualenv
|
|
||||||
return exe
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
|
||||||
expected = os.path.join(prefix, 'python.exe')
|
|
||||||
else:
|
|
||||||
expected = os.path.join(prefix, 'bin', os.path.basename(exe))
|
|
||||||
|
|
||||||
if os.path.exists(expected):
|
|
||||||
return expected
|
|
||||||
else:
|
|
||||||
return exe
|
|
||||||
|
|
||||||
|
|
||||||
def make_venv(envdir, python):
|
|
||||||
cmd_output(orig_py_exe(python), '-mvenv', envdir, cwd='/')
|
|
||||||
|
|
||||||
|
|
||||||
_interface = python.py_interface(ENVIRONMENT_DIR, make_venv)
|
|
||||||
in_env, healthy, run_hook, install_environment = _interface
|
|
||||||
278
pre_commit/languages/r.py
Normal file
278
pre_commit/languages/r.py
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import UNSET
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.util import cmd_output
|
||||||
|
from pre_commit.util import win_exe
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'renv'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
|
||||||
|
_RENV_ACTIVATED_OPTS = (
|
||||||
|
'--no-save', '--no-restore', '--no-site-file', '--no-environ',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_r(
|
||||||
|
code: str, *,
|
||||||
|
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
||||||
|
cli_opts: Sequence[str],
|
||||||
|
) -> str:
|
||||||
|
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
|
||||||
|
_, out, _ = cmd_output(
|
||||||
|
_rscript_exec(), *cli_opts, f, *args, cwd=cwd,
|
||||||
|
)
|
||||||
|
return out.rstrip('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_r_in_renv(
|
||||||
|
code: str, *,
|
||||||
|
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
||||||
|
) -> str:
|
||||||
|
return _execute_r(
|
||||||
|
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
|
||||||
|
cli_opts=_RENV_ACTIVATED_OPTS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _execute_vanilla_r(
|
||||||
|
code: str, *,
|
||||||
|
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
||||||
|
) -> str:
|
||||||
|
return _execute_r(
|
||||||
|
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
|
||||||
|
cli_opts=('--vanilla',),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
|
||||||
|
return _execute_r_in_renv(
|
||||||
|
'cat(renv::settings$r.version())',
|
||||||
|
prefix=prefix, version=version,
|
||||||
|
cwd=envdir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
|
||||||
|
return _execute_r_in_renv(
|
||||||
|
'cat(as.character(getRversion()))',
|
||||||
|
prefix=prefix, version=version,
|
||||||
|
cwd=envdir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_current_r_version(
|
||||||
|
envdir: str, prefix: Prefix, version: str,
|
||||||
|
) -> None:
|
||||||
|
_execute_r_in_renv(
|
||||||
|
'renv::settings$r.version(as.character(getRversion()))',
|
||||||
|
prefix=prefix, version=version,
|
||||||
|
cwd=envdir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def health_check(prefix: Prefix, version: str) -> str | None:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
|
r_version_installation = _read_installed_version(
|
||||||
|
envdir=envdir, prefix=prefix, version=version,
|
||||||
|
)
|
||||||
|
r_version_current_executable = _read_executable_version(
|
||||||
|
envdir=envdir, prefix=prefix, version=version,
|
||||||
|
)
|
||||||
|
if r_version_installation in {'NULL', ''}:
|
||||||
|
return (
|
||||||
|
f'Hooks were installed with an unknown R version. R version for '
|
||||||
|
f'hook repo now set to {r_version_current_executable}'
|
||||||
|
)
|
||||||
|
elif r_version_installation != r_version_current_executable:
|
||||||
|
return (
|
||||||
|
f'Hooks were installed for R version {r_version_installation}, '
|
||||||
|
f'but current R executable has version '
|
||||||
|
f'{r_version_current_executable}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _r_code_in_tempfile(code: str) -> Generator[str]:
|
||||||
|
"""
|
||||||
|
To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}`
|
||||||
|
but use `Rscript [options] path/to/file_with_expr.R`
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
fname = os.path.join(tmpdir, 'script.R')
|
||||||
|
with open(fname, 'w') as f:
|
||||||
|
f.write(_inline_r_setup(textwrap.dedent(code)))
|
||||||
|
yield fname
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(venv: str) -> PatchesT:
|
||||||
|
return (
|
||||||
|
('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
|
||||||
|
('RENV_PROJECT', UNSET),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
with envcontext(get_env_patch(envdir)):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def _prefix_if_file_entry(
|
||||||
|
entry: list[str],
|
||||||
|
prefix: Prefix,
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
) -> Sequence[str]:
|
||||||
|
if entry[1] == '-e' or is_local:
|
||||||
|
return entry[1:]
|
||||||
|
else:
|
||||||
|
return (prefix.path(entry[1]),)
|
||||||
|
|
||||||
|
|
||||||
|
def _rscript_exec() -> str:
|
||||||
|
r_home = os.environ.get('R_HOME')
|
||||||
|
if r_home is None:
|
||||||
|
return 'Rscript'
|
||||||
|
else:
|
||||||
|
return os.path.join(r_home, 'bin', win_exe('Rscript'))
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_validate(entry: list[str]) -> None:
|
||||||
|
"""
|
||||||
|
Allowed entries:
|
||||||
|
# Rscript -e expr
|
||||||
|
# Rscript path/to/file
|
||||||
|
"""
|
||||||
|
if entry[0] != 'Rscript':
|
||||||
|
raise ValueError('entry must start with `Rscript`.')
|
||||||
|
|
||||||
|
if entry[1] == '-e':
|
||||||
|
if len(entry) > 3:
|
||||||
|
raise ValueError('You can supply at most one expression.')
|
||||||
|
elif len(entry) > 2:
|
||||||
|
raise ValueError(
|
||||||
|
'The only valid syntax is `Rscript -e {expr}`'
|
||||||
|
'or `Rscript path/to/hook/script`',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_from_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
cmd = shlex.split(entry)
|
||||||
|
_entry_validate(cmd)
|
||||||
|
|
||||||
|
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local)
|
||||||
|
return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
lang_base.assert_version_default('r', version)
|
||||||
|
|
||||||
|
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
os.makedirs(env_dir, exist_ok=True)
|
||||||
|
shutil.copy(prefix.path('renv.lock'), env_dir)
|
||||||
|
shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv'))
|
||||||
|
|
||||||
|
r_code_inst_environment = f"""\
|
||||||
|
prefix_dir <- {prefix.prefix_dir!r}
|
||||||
|
options(
|
||||||
|
repos = c(CRAN = "https://cran.rstudio.com"),
|
||||||
|
renv.consent = TRUE
|
||||||
|
)
|
||||||
|
source("renv/activate.R")
|
||||||
|
renv::restore()
|
||||||
|
activate_statement <- paste0(
|
||||||
|
'suppressWarnings({{',
|
||||||
|
'old <- setwd("', getwd(), '"); ',
|
||||||
|
'source("renv/activate.R"); ',
|
||||||
|
'setwd(old); ',
|
||||||
|
'renv::load("', getwd(), '");}})'
|
||||||
|
)
|
||||||
|
writeLines(activate_statement, 'activate.R')
|
||||||
|
is_package <- tryCatch(
|
||||||
|
{{
|
||||||
|
path_desc <- file.path(prefix_dir, 'DESCRIPTION')
|
||||||
|
suppressWarnings(desc <- read.dcf(path_desc))
|
||||||
|
"Package" %in% colnames(desc)
|
||||||
|
}},
|
||||||
|
error = function(...) FALSE
|
||||||
|
)
|
||||||
|
if (is_package) {{
|
||||||
|
renv::install(prefix_dir)
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
_execute_vanilla_r(
|
||||||
|
r_code_inst_environment,
|
||||||
|
prefix=prefix, version=version, cwd=env_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
|
||||||
|
if additional_dependencies:
|
||||||
|
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
|
||||||
|
_execute_r_in_renv(
|
||||||
|
code=r_code_inst_add, prefix=prefix, version=version,
|
||||||
|
args=additional_dependencies,
|
||||||
|
cwd=env_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _inline_r_setup(code: str) -> str:
|
||||||
|
"""
|
||||||
|
Some behaviour of R cannot be configured via env variables, but can
|
||||||
|
only be configured via R options once R has started. These are set here.
|
||||||
|
"""
|
||||||
|
with_option = [
|
||||||
|
textwrap.dedent("""\
|
||||||
|
options(
|
||||||
|
install.packages.compile.from.source = "never",
|
||||||
|
pkgType = "binary"
|
||||||
|
)
|
||||||
|
"""),
|
||||||
|
code,
|
||||||
|
]
|
||||||
|
return '\n'.join(with_option)
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local)
|
||||||
|
return lang_base.run_xargs(
|
||||||
|
cmd,
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
@ -1,128 +1,145 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import functools
|
||||||
|
import importlib.resources
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import IO
|
||||||
|
|
||||||
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
|
from pre_commit.envcontext import UNSET
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import CalledProcessError
|
from pre_commit.util import CalledProcessError
|
||||||
from pre_commit.util import clean_path_on_failure
|
|
||||||
from pre_commit.util import resource_filename
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'rbenv'
|
ENVIRONMENT_DIR = 'rbenv'
|
||||||
get_default_version = helpers.basic_get_default_version
|
health_check = lang_base.basic_health_check
|
||||||
healthy = helpers.basic_healthy
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
def get_env_patch(venv, language_version): # pragma: windows no cover
|
def _resource_bytesio(filename: str) -> IO[bytes]:
|
||||||
patches = (
|
files = importlib.resources.files('pre_commit.resources')
|
||||||
|
return files.joinpath(filename).open('rb')
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def get_default_version() -> str:
|
||||||
|
if all(lang_base.exe_exists(exe) for exe in ('ruby', 'gem')):
|
||||||
|
return 'system'
|
||||||
|
else:
|
||||||
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(
|
||||||
|
venv: str,
|
||||||
|
language_version: str,
|
||||||
|
) -> PatchesT:
|
||||||
|
patches: PatchesT = (
|
||||||
('GEM_HOME', os.path.join(venv, 'gems')),
|
('GEM_HOME', os.path.join(venv, 'gems')),
|
||||||
('RBENV_ROOT', venv),
|
('GEM_PATH', UNSET),
|
||||||
('BUNDLE_IGNORE_CONFIG', '1'),
|
('BUNDLE_IGNORE_CONFIG', '1'),
|
||||||
(
|
|
||||||
'PATH', (
|
|
||||||
os.path.join(venv, 'gems', 'bin'), os.pathsep,
|
|
||||||
os.path.join(venv, 'shims'), os.pathsep,
|
|
||||||
os.path.join(venv, 'bin'), os.pathsep, Var('PATH'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if language_version != 'default':
|
if language_version == 'system':
|
||||||
|
patches += (
|
||||||
|
(
|
||||||
|
'PATH', (
|
||||||
|
os.path.join(venv, 'gems', 'bin'), os.pathsep,
|
||||||
|
Var('PATH'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else: # pragma: win32 no cover
|
||||||
|
patches += (
|
||||||
|
('RBENV_ROOT', venv),
|
||||||
|
(
|
||||||
|
'PATH', (
|
||||||
|
os.path.join(venv, 'gems', 'bin'), os.pathsep,
|
||||||
|
os.path.join(venv, 'shims'), os.pathsep,
|
||||||
|
os.path.join(venv, 'bin'), os.pathsep, Var('PATH'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if language_version not in {'system', 'default'}: # pragma: win32 no cover
|
||||||
patches += (('RBENV_VERSION', language_version),)
|
patches += (('RBENV_VERSION', language_version),)
|
||||||
|
|
||||||
return patches
|
return patches
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def in_env(prefix, language_version): # pragma: windows no cover
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
envdir = prefix.path(
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, language_version),
|
with envcontext(get_env_patch(envdir, version)):
|
||||||
)
|
|
||||||
with envcontext(get_env_patch(envdir, language_version)):
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def _install_rbenv(prefix, version='default'): # pragma: windows no cover
|
def _extract_resource(filename: str, dest: str) -> None:
|
||||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
with _resource_bytesio(filename) as bio:
|
||||||
|
with tarfile.open(fileobj=bio) as tf:
|
||||||
|
tf.extractall(dest)
|
||||||
|
|
||||||
with tarfile.open(resource_filename('rbenv.tar.gz')) as tf:
|
|
||||||
tf.extractall(prefix.path('.'))
|
def _install_rbenv(
|
||||||
shutil.move(prefix.path('rbenv'), prefix.path(directory))
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
) -> None: # pragma: win32 no cover
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
|
_extract_resource('rbenv.tar.gz', prefix.path('.'))
|
||||||
|
shutil.move(prefix.path('rbenv'), envdir)
|
||||||
|
|
||||||
# Only install ruby-build if the version is specified
|
# Only install ruby-build if the version is specified
|
||||||
if version != 'default':
|
if version != C.DEFAULT:
|
||||||
# ruby-download
|
plugins_dir = os.path.join(envdir, 'plugins')
|
||||||
with tarfile.open(resource_filename('ruby-download.tar.gz')) as tf:
|
_extract_resource('ruby-download.tar.gz', plugins_dir)
|
||||||
tf.extractall(prefix.path(directory, 'plugins'))
|
_extract_resource('ruby-build.tar.gz', plugins_dir)
|
||||||
|
|
||||||
# ruby-build
|
|
||||||
with tarfile.open(resource_filename('ruby-build.tar.gz')) as tf:
|
|
||||||
tf.extractall(prefix.path(directory, 'plugins'))
|
|
||||||
|
|
||||||
activate_path = prefix.path(directory, 'bin', 'activate')
|
|
||||||
with io.open(activate_path, 'w') as activate_file:
|
|
||||||
# This is similar to how you would install rbenv to your home directory
|
|
||||||
# However we do a couple things to make the executables exposed and
|
|
||||||
# configure it to work in our directory.
|
|
||||||
# We also modify the PS1 variable for manual debugging sake.
|
|
||||||
activate_file.write(
|
|
||||||
'#!/usr/bin/env bash\n'
|
|
||||||
"export RBENV_ROOT='{directory}'\n"
|
|
||||||
'export PATH="$RBENV_ROOT/bin:$PATH"\n'
|
|
||||||
'eval "$(rbenv init -)"\n'
|
|
||||||
'export PS1="(rbenv)$PS1"\n'
|
|
||||||
# This lets us install gems in an isolated and repeatable
|
|
||||||
# directory
|
|
||||||
"export GEM_HOME='{directory}/gems'\n"
|
|
||||||
'export PATH="$GEM_HOME/bin:$PATH"\n'
|
|
||||||
'\n'.format(directory=prefix.path(directory)),
|
|
||||||
)
|
|
||||||
|
|
||||||
# If we aren't using the system ruby, add a version here
|
|
||||||
if version != 'default':
|
|
||||||
activate_file.write('export RBENV_VERSION="{}"\n'.format(version))
|
|
||||||
|
|
||||||
|
|
||||||
def _install_ruby(runner, version): # pragma: windows no cover
|
def _install_ruby(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
) -> None: # pragma: win32 no cover
|
||||||
try:
|
try:
|
||||||
helpers.run_setup_cmd(runner, ('rbenv', 'download', version))
|
lang_base.setup_cmd(prefix, ('rbenv', 'download', version))
|
||||||
except CalledProcessError: # pragma: no cover (usually find with download)
|
except CalledProcessError: # pragma: no cover (usually find with download)
|
||||||
# Failed to download from mirror for some reason, build it instead
|
# Failed to download from mirror for some reason, build it instead
|
||||||
helpers.run_setup_cmd(runner, ('rbenv', 'install', version))
|
lang_base.setup_cmd(prefix, ('rbenv', 'install', version))
|
||||||
|
|
||||||
|
|
||||||
def install_environment(
|
def install_environment(
|
||||||
prefix, version, additional_dependencies,
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||||
): # pragma: windows no cover
|
) -> None:
|
||||||
additional_dependencies = tuple(additional_dependencies)
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
|
||||||
with clean_path_on_failure(prefix.path(directory)):
|
if version != 'system': # pragma: win32 no cover
|
||||||
# TODO: this currently will fail if there's no version specified and
|
_install_rbenv(prefix, version)
|
||||||
# there's no system ruby installed. Is this ok?
|
|
||||||
_install_rbenv(prefix, version=version)
|
|
||||||
with in_env(prefix, version):
|
with in_env(prefix, version):
|
||||||
# Need to call this before installing so rbenv's directories are
|
# Need to call this before installing so rbenv's directories
|
||||||
# set up
|
# are set up
|
||||||
helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
|
lang_base.setup_cmd(prefix, ('rbenv', 'init', '-'))
|
||||||
if version != 'default':
|
if version != C.DEFAULT:
|
||||||
_install_ruby(prefix, version)
|
_install_ruby(prefix, version)
|
||||||
# Need to call this after installing to set up the shims
|
# Need to call this after installing to set up the shims
|
||||||
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
|
lang_base.setup_cmd(prefix, ('rbenv', 'rehash'))
|
||||||
helpers.run_setup_cmd(
|
|
||||||
prefix, ('gem', 'build') + prefix.star('.gemspec'),
|
|
||||||
)
|
|
||||||
helpers.run_setup_cmd(
|
|
||||||
prefix,
|
|
||||||
('gem', 'install', '--no-ri', '--no-rdoc') +
|
|
||||||
prefix.star('.gem') + additional_dependencies,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
with in_env(prefix, version):
|
||||||
def run_hook(prefix, hook, file_args): # pragma: windows no cover
|
lang_base.setup_cmd(
|
||||||
with in_env(prefix, hook['language_version']):
|
prefix, ('gem', 'build', *prefix.star('.gemspec')),
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
)
|
||||||
|
lang_base.setup_cmd(
|
||||||
|
prefix,
|
||||||
|
(
|
||||||
|
'gem', 'install',
|
||||||
|
'--no-document', '--no-format-executable',
|
||||||
|
'--no-user-install',
|
||||||
|
'--install-dir', os.path.join(envdir, 'gems'),
|
||||||
|
'--bindir', os.path.join(envdir, 'gems', 'bin'),
|
||||||
|
*prefix.star('.gem'), *additional_dependencies,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,121 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import functools
|
||||||
import os.path
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import toml
|
import pre_commit.constants as C
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit import parse_shebang
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.util import cmd_output_b
|
||||||
from pre_commit.util import cmd_output
|
from pre_commit.util import make_executable
|
||||||
from pre_commit.xargs import xargs
|
from pre_commit.util import win_exe
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'rustenv'
|
ENVIRONMENT_DIR = 'rustenv'
|
||||||
get_default_version = helpers.basic_get_default_version
|
health_check = lang_base.basic_health_check
|
||||||
healthy = helpers.basic_healthy
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
|
|
||||||
def get_env_patch(target_dir):
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def get_default_version() -> str:
|
||||||
|
# If rust is already installed, we can save a bunch of setup time by
|
||||||
|
# using the installed version.
|
||||||
|
#
|
||||||
|
# Just detecting the executable does not suffice, because if rustup is
|
||||||
|
# installed but no toolchain is available, then `cargo` exists but
|
||||||
|
# cannot be used without installing a toolchain first.
|
||||||
|
if cmd_output_b('cargo', '--version', check=False, cwd='/')[0] == 0:
|
||||||
|
return 'system'
|
||||||
|
else:
|
||||||
|
return C.DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def _rust_toolchain(language_version: str) -> str:
|
||||||
|
"""Transform the language version into a rust toolchain version."""
|
||||||
|
if language_version == C.DEFAULT:
|
||||||
|
return 'stable'
|
||||||
|
else:
|
||||||
|
return language_version
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_patch(target_dir: str, version: str) -> PatchesT:
|
||||||
return (
|
return (
|
||||||
(
|
('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
|
||||||
'PATH',
|
# Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
|
||||||
(os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH')),
|
# toolchain
|
||||||
|
*(
|
||||||
|
(('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),)
|
||||||
|
if version != 'system' else ()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def in_env(prefix):
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
target_dir = prefix.path(
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
with envcontext(get_env_patch(envdir, version)):
|
||||||
)
|
|
||||||
with envcontext(get_env_patch(target_dir)):
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def _add_dependencies(cargo_toml_path, additional_dependencies):
|
def _add_dependencies(
|
||||||
with open(cargo_toml_path, 'r+') as f:
|
prefix: Prefix,
|
||||||
cargo_toml = toml.load(f)
|
additional_dependencies: set[str],
|
||||||
cargo_toml.setdefault('dependencies', {})
|
) -> None:
|
||||||
for dep in additional_dependencies:
|
crates = []
|
||||||
name, _, spec = dep.partition(':')
|
for dep in additional_dependencies:
|
||||||
cargo_toml['dependencies'][name] = spec or '*'
|
name, _, spec = dep.partition(':')
|
||||||
f.seek(0)
|
crate = f'{name}@{spec or "*"}'
|
||||||
toml.dump(cargo_toml, f)
|
crates.append(crate)
|
||||||
f.truncate()
|
|
||||||
|
lang_base.setup_cmd(prefix, ('cargo', 'add', *crates))
|
||||||
|
|
||||||
|
|
||||||
def install_environment(prefix, version, additional_dependencies):
|
def install_rust_with_toolchain(toolchain: str, envdir: str) -> None:
|
||||||
helpers.assert_version_default('rust', version)
|
with tempfile.TemporaryDirectory() as rustup_dir:
|
||||||
directory = prefix.path(
|
with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))):
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
# acquire `rustup` if not present
|
||||||
)
|
if parse_shebang.find_executable('rustup') is None:
|
||||||
|
# We did not detect rustup and need to download it first.
|
||||||
|
if sys.platform == 'win32': # pragma: win32 cover
|
||||||
|
url = 'https://win.rustup.rs/x86_64'
|
||||||
|
else: # pragma: win32 no cover
|
||||||
|
url = 'https://sh.rustup.rs'
|
||||||
|
|
||||||
|
resp = urllib.request.urlopen(url)
|
||||||
|
|
||||||
|
rustup_init = os.path.join(rustup_dir, win_exe('rustup-init'))
|
||||||
|
with open(rustup_init, 'wb') as f:
|
||||||
|
shutil.copyfileobj(resp, f)
|
||||||
|
make_executable(rustup_init)
|
||||||
|
|
||||||
|
# install rustup into `$CARGO_HOME/bin`
|
||||||
|
cmd_output_b(
|
||||||
|
rustup_init, '-y', '--quiet', '--no-modify-path',
|
||||||
|
'--default-toolchain', 'none',
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd_output_b(
|
||||||
|
'rustup', 'toolchain', 'install', '--no-self-update',
|
||||||
|
toolchain,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_environment(
|
||||||
|
prefix: Prefix,
|
||||||
|
version: str,
|
||||||
|
additional_dependencies: Sequence[str],
|
||||||
|
) -> None:
|
||||||
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
|
|
||||||
# There are two cases where we might want to specify more dependencies:
|
# There are two cases where we might want to specify more dependencies:
|
||||||
# as dependencies for the library being built, and as binary packages
|
# as dependencies for the library being built, and as binary packages
|
||||||
|
|
@ -69,26 +132,29 @@ def install_environment(prefix, version, additional_dependencies):
|
||||||
}
|
}
|
||||||
lib_deps = set(additional_dependencies) - cli_deps
|
lib_deps = set(additional_dependencies) - cli_deps
|
||||||
|
|
||||||
if len(lib_deps) > 0:
|
packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
|
||||||
_add_dependencies(prefix.path('Cargo.toml'), lib_deps)
|
for cli_dep in cli_deps:
|
||||||
|
cli_dep = cli_dep.removeprefix('cli:')
|
||||||
|
package, _, crate_version = cli_dep.partition(':')
|
||||||
|
if crate_version != '':
|
||||||
|
packages_to_install.add((package, '--version', crate_version))
|
||||||
|
else:
|
||||||
|
packages_to_install.add((package,))
|
||||||
|
|
||||||
with clean_path_on_failure(directory):
|
with contextlib.ExitStack() as ctx:
|
||||||
packages_to_install = {()}
|
ctx.enter_context(in_env(prefix, version))
|
||||||
for cli_dep in cli_deps:
|
|
||||||
cli_dep = cli_dep[len('cli:'):]
|
|
||||||
package, _, version = cli_dep.partition(':')
|
|
||||||
if version != '':
|
|
||||||
packages_to_install.add((package, '--version', version))
|
|
||||||
else:
|
|
||||||
packages_to_install.add((package,))
|
|
||||||
|
|
||||||
for package in packages_to_install:
|
if version != 'system':
|
||||||
cmd_output(
|
install_rust_with_toolchain(_rust_toolchain(version), envdir)
|
||||||
'cargo', 'install', '--bins', '--root', directory, *package,
|
|
||||||
cwd=prefix.prefix_dir
|
tmpdir = ctx.enter_context(tempfile.TemporaryDirectory())
|
||||||
|
ctx.enter_context(envcontext((('RUSTUP_HOME', tmpdir),)))
|
||||||
|
|
||||||
|
if len(lib_deps) > 0:
|
||||||
|
_add_dependencies(prefix, lib_deps)
|
||||||
|
|
||||||
|
for args in packages_to_install:
|
||||||
|
cmd_output_b(
|
||||||
|
'cargo', 'install', '--bins', '--root', envdir, *args,
|
||||||
|
cwd=prefix.prefix_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
with in_env(prefix):
|
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from pre_commit.languages import helpers
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = None
|
|
||||||
get_default_version = helpers.basic_get_default_version
|
|
||||||
healthy = helpers.basic_healthy
|
|
||||||
install_environment = helpers.no_install
|
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
cmd = helpers.to_cmd(hook)
|
|
||||||
cmd = (prefix.path(cmd[0]),) + cmd[1:]
|
|
||||||
return xargs(cmd, file_args)
|
|
||||||
|
|
@ -1,56 +1,50 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
from collections.abc import Generator
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
from pre_commit.envcontext import envcontext
|
from pre_commit.envcontext import envcontext
|
||||||
|
from pre_commit.envcontext import PatchesT
|
||||||
from pre_commit.envcontext import Var
|
from pre_commit.envcontext import Var
|
||||||
from pre_commit.languages import helpers
|
from pre_commit.prefix import Prefix
|
||||||
from pre_commit.util import clean_path_on_failure
|
from pre_commit.util import cmd_output_b
|
||||||
from pre_commit.util import cmd_output
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = 'swift_env'
|
|
||||||
get_default_version = helpers.basic_get_default_version
|
|
||||||
healthy = helpers.basic_healthy
|
|
||||||
BUILD_DIR = '.build'
|
BUILD_DIR = '.build'
|
||||||
BUILD_CONFIG = 'release'
|
BUILD_CONFIG = 'release'
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = 'swift_env'
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
|
|
||||||
def get_env_patch(venv): # pragma: windows no cover
|
|
||||||
|
def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
|
||||||
bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
|
bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
|
||||||
return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
|
return (('PATH', (bin_path, os.pathsep, Var('PATH'))),)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager # pragma: win32 no cover
|
||||||
def in_env(prefix): # pragma: windows no cover
|
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
||||||
envdir = prefix.path(
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
|
||||||
)
|
|
||||||
with envcontext(get_env_patch(envdir)):
|
with envcontext(get_env_patch(envdir)):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
def install_environment(
|
def install_environment(
|
||||||
prefix, version, additional_dependencies,
|
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||||
): # pragma: windows no cover
|
) -> None: # pragma: win32 no cover
|
||||||
helpers.assert_version_default('swift', version)
|
lang_base.assert_version_default('swift', version)
|
||||||
helpers.assert_no_additional_deps('swift', additional_dependencies)
|
lang_base.assert_no_additional_deps('swift', additional_dependencies)
|
||||||
directory = prefix.path(
|
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||||
helpers.environment_dir(ENVIRONMENT_DIR, 'default'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build the swift package
|
# Build the swift package
|
||||||
with clean_path_on_failure(directory):
|
os.mkdir(envdir)
|
||||||
os.mkdir(directory)
|
cmd_output_b(
|
||||||
cmd_output(
|
'swift', 'build',
|
||||||
'swift', 'build',
|
'--package-path', prefix.prefix_dir,
|
||||||
'-C', prefix.prefix_dir,
|
'-c', BUILD_CONFIG,
|
||||||
'-c', BUILD_CONFIG,
|
'--build-path', os.path.join(envdir, BUILD_DIR),
|
||||||
'--build-path', os.path.join(directory, BUILD_DIR),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args): # pragma: windows no cover
|
|
||||||
with in_env(prefix):
|
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from pre_commit.languages import helpers
|
|
||||||
from pre_commit.xargs import xargs
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_DIR = None
|
|
||||||
get_default_version = helpers.basic_get_default_version
|
|
||||||
healthy = helpers.basic_healthy
|
|
||||||
install_environment = helpers.no_install
|
|
||||||
|
|
||||||
|
|
||||||
def run_hook(prefix, hook, file_args):
|
|
||||||
return xargs(helpers.to_cmd(hook), file_args)
|
|
||||||
10
pre_commit/languages/unsupported.py
Normal file
10
pre_commit/languages/unsupported.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = None
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
install_environment = lang_base.no_install
|
||||||
|
in_env = lang_base.no_env
|
||||||
|
run_hook = lang_base.basic_run_hook
|
||||||
32
pre_commit/languages/unsupported_script.py
Normal file
32
pre_commit/languages/unsupported_script.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import lang_base
|
||||||
|
from pre_commit.prefix import Prefix
|
||||||
|
|
||||||
|
ENVIRONMENT_DIR = None
|
||||||
|
get_default_version = lang_base.basic_get_default_version
|
||||||
|
health_check = lang_base.basic_health_check
|
||||||
|
install_environment = lang_base.no_install
|
||||||
|
in_env = lang_base.no_env
|
||||||
|
|
||||||
|
|
||||||
|
def run_hook(
|
||||||
|
prefix: Prefix,
|
||||||
|
entry: str,
|
||||||
|
args: Sequence[str],
|
||||||
|
file_args: Sequence[str],
|
||||||
|
*,
|
||||||
|
is_local: bool,
|
||||||
|
require_serial: bool,
|
||||||
|
color: bool,
|
||||||
|
) -> tuple[int, bytes]:
|
||||||
|
cmd = lang_base.hook_cmd(entry, args)
|
||||||
|
cmd = (prefix.path(cmd[0]), *cmd[1:])
|
||||||
|
return lang_base.run_xargs(
|
||||||
|
cmd,
|
||||||
|
file_args,
|
||||||
|
require_serial=require_serial,
|
||||||
|
color=color,
|
||||||
|
)
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
from pre_commit import color
|
from pre_commit import color
|
||||||
from pre_commit import output
|
from pre_commit import output
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('pre_commit')
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
LOG_LEVEL_COLORS = {
|
LOG_LEVEL_COLORS = {
|
||||||
|
|
@ -17,23 +18,25 @@ LOG_LEVEL_COLORS = {
|
||||||
|
|
||||||
|
|
||||||
class LoggingHandler(logging.Handler):
|
class LoggingHandler(logging.Handler):
|
||||||
def __init__(self, use_color):
|
def __init__(self, use_color: bool) -> None:
|
||||||
super(LoggingHandler, self).__init__()
|
super().__init__()
|
||||||
self.use_color = use_color
|
self.use_color = use_color
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
output.write_line(
|
level_msg = color.format_color(
|
||||||
'{} {}'.format(
|
f'[{record.levelname}]',
|
||||||
color.format_color(
|
LOG_LEVEL_COLORS[record.levelname],
|
||||||
'[{}]'.format(record.levelname),
|
self.use_color,
|
||||||
LOG_LEVEL_COLORS[record.levelname],
|
|
||||||
self.use_color,
|
|
||||||
),
|
|
||||||
record.getMessage(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
output.write_line(f'{level_msg} {record.getMessage()}')
|
||||||
|
|
||||||
|
|
||||||
def add_logging_handler(*args, **kwargs):
|
@contextlib.contextmanager
|
||||||
logger.addHandler(LoggingHandler(*args, **kwargs))
|
def logging_handler(use_color: bool) -> Generator[None]:
|
||||||
|
handler = LoggingHandler(use_color)
|
||||||
|
logger.addHandler(handler)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
logger.removeHandler(handler)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit import color
|
from pre_commit import clientlib
|
||||||
from pre_commit import five
|
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
|
from pre_commit.color import add_color_option
|
||||||
|
from pre_commit.commands import hazmat
|
||||||
from pre_commit.commands.autoupdate import autoupdate
|
from pre_commit.commands.autoupdate import autoupdate
|
||||||
from pre_commit.commands.clean import clean
|
from pre_commit.commands.clean import clean
|
||||||
|
from pre_commit.commands.gc import gc
|
||||||
|
from pre_commit.commands.hook_impl import hook_impl
|
||||||
|
from pre_commit.commands.init_templatedir import init_templatedir
|
||||||
from pre_commit.commands.install_uninstall import install
|
from pre_commit.commands.install_uninstall import install
|
||||||
from pre_commit.commands.install_uninstall import install_hooks
|
from pre_commit.commands.install_uninstall import install_hooks
|
||||||
from pre_commit.commands.install_uninstall import uninstall
|
from pre_commit.commands.install_uninstall import uninstall
|
||||||
|
|
@ -18,9 +23,10 @@ from pre_commit.commands.migrate_config import migrate_config
|
||||||
from pre_commit.commands.run import run
|
from pre_commit.commands.run import run
|
||||||
from pre_commit.commands.sample_config import sample_config
|
from pre_commit.commands.sample_config import sample_config
|
||||||
from pre_commit.commands.try_repo import try_repo
|
from pre_commit.commands.try_repo import try_repo
|
||||||
|
from pre_commit.commands.validate_config import validate_config
|
||||||
|
from pre_commit.commands.validate_manifest import validate_manifest
|
||||||
from pre_commit.error_handler import error_handler
|
from pre_commit.error_handler import error_handler
|
||||||
from pre_commit.logging_handler import add_logging_handler
|
from pre_commit.logging_handler import logging_handler
|
||||||
from pre_commit.runner import Runner
|
|
||||||
from pre_commit.store import Store
|
from pre_commit.store import Store
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,81 +38,239 @@ logger = logging.getLogger('pre_commit')
|
||||||
# pyvenv
|
# pyvenv
|
||||||
os.environ.pop('__PYVENV_LAUNCHER__', None)
|
os.environ.pop('__PYVENV_LAUNCHER__', None)
|
||||||
|
|
||||||
|
# https://github.com/getsentry/snuba/pull/5388
|
||||||
|
os.environ.pop('PYTHONEXECUTABLE', None)
|
||||||
|
|
||||||
def _add_color_option(parser):
|
COMMANDS_NO_GIT = {
|
||||||
parser.add_argument(
|
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
|
||||||
'--color', default='auto', type=color.use_color,
|
'validate-config', 'validate-manifest',
|
||||||
metavar='{' + ','.join(color.COLOR_CHOICES) + '}',
|
}
|
||||||
help='Whether to use color in output. Defaults to `%(default)s`.',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _add_config_option(parser):
|
def _add_config_option(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config', default=C.CONFIG_FILE,
|
'-c', '--config', default=C.CONFIG_FILE,
|
||||||
help='Path to alternate config file',
|
help='Path to alternate config file',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_hook_type_option(parser):
|
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-t', '--hook-type', choices=('pre-commit', 'pre-push', 'commit-msg'),
|
'-t', '--hook-type',
|
||||||
default='pre-commit',
|
choices=clientlib.HOOK_TYPES, action='append', dest='hook_types',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_run_options(parser):
|
def _add_run_options(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument('hook', nargs='?', help='A single hook-id to run')
|
parser.add_argument('hook', nargs='?', help='A single hook-id to run')
|
||||||
parser.add_argument('--verbose', '-v', action='store_true', default=False)
|
parser.add_argument('--verbose', '-v', action='store_true')
|
||||||
parser.add_argument(
|
|
||||||
'--origin', '-o',
|
|
||||||
help="The origin branch's commit_id when using `git push`.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--source', '-s',
|
|
||||||
help="The remote branch's commit_id when using `git push`.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--commit-msg-filename',
|
|
||||||
help='Filename to check when running during `commit-msg`',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--hook-stage', choices=C.STAGES, default='commit',
|
|
||||||
help='The stage during which the hook is fired. One of %(choices)s',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--show-diff-on-failure', action='store_true',
|
|
||||||
help='When hooks fail, run `git diff` directly afterward.',
|
|
||||||
)
|
|
||||||
mutex_group = parser.add_mutually_exclusive_group(required=False)
|
mutex_group = parser.add_mutually_exclusive_group(required=False)
|
||||||
mutex_group.add_argument(
|
mutex_group.add_argument(
|
||||||
'--all-files', '-a', action='store_true', default=False,
|
'--all-files', '-a', action='store_true',
|
||||||
help='Run on all the files in the repo.',
|
help='Run on all the files in the repo.',
|
||||||
)
|
)
|
||||||
mutex_group.add_argument(
|
mutex_group.add_argument(
|
||||||
'--files', nargs='*', default=[],
|
'--files', nargs='*', default=[],
|
||||||
help='Specific filenames to run hooks on.',
|
help='Specific filenames to run hooks on.',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--show-diff-on-failure', action='store_true',
|
||||||
|
help='When hooks fail, run `git diff` directly afterward.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--fail-fast', action='store_true',
|
||||||
|
help='Stop after the first failing hook.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--hook-stage',
|
||||||
|
choices=clientlib.STAGES,
|
||||||
|
type=clientlib.transform_stage,
|
||||||
|
default='pre-commit',
|
||||||
|
help='The stage during which the hook is fired. One of %(choices)s',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--remote-branch', help='Remote branch ref used by `git push`.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--local-branch', help='Local branch ref used by `git push`.',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--from-ref', '--source', '-s',
|
||||||
|
help=(
|
||||||
|
'(for usage with `--to-ref`) -- this option represents the '
|
||||||
|
'original ref in a `from_ref...to_ref` diff expression. '
|
||||||
|
'For `pre-push` hooks, this represents the branch you are pushing '
|
||||||
|
'to. '
|
||||||
|
'For `post-checkout` hooks, this represents the branch that was '
|
||||||
|
'previously checked out.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--to-ref', '--origin', '-o',
|
||||||
|
help=(
|
||||||
|
'(for usage with `--from-ref`) -- this option represents the '
|
||||||
|
'destination ref in a `from_ref...to_ref` diff expression. '
|
||||||
|
'For `pre-push` hooks, this represents the branch being pushed. '
|
||||||
|
'For `post-checkout` hooks, this represents the branch that is '
|
||||||
|
'now checked out.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--pre-rebase-upstream', help=(
|
||||||
|
'The upstream from which the series was forked.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--pre-rebase-branch', help=(
|
||||||
|
'The branch being rebased, and is not set when '
|
||||||
|
'rebasing the current branch.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--commit-msg-filename',
|
||||||
|
help='Filename to check when running during `commit-msg`',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--prepare-commit-message-source',
|
||||||
|
help=(
|
||||||
|
'Source of the commit message '
|
||||||
|
'(typically the second argument to .git/hooks/prepare-commit-msg)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--commit-object-name',
|
||||||
|
help=(
|
||||||
|
'Commit object name '
|
||||||
|
'(typically the third argument to .git/hooks/prepare-commit-msg)'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--remote-name', help='Remote name used by `git push`.',
|
||||||
|
)
|
||||||
|
parser.add_argument('--remote-url', help='Remote url used by `git push`.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--checkout-type',
|
||||||
|
help=(
|
||||||
|
'Indicates whether the checkout was a branch checkout '
|
||||||
|
'(changing branches, flag=1) or a file checkout (retrieving a '
|
||||||
|
'file from the index, flag=0).'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--is-squash-merge',
|
||||||
|
help=(
|
||||||
|
'During a post-merge hook, indicates whether the merge was a '
|
||||||
|
'squash merge'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--rewrite-command',
|
||||||
|
help=(
|
||||||
|
'During a post-rewrite hook, specifies the command that invoked '
|
||||||
|
'the rewrite'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
|
||||||
|
# `--config` was specified relative to the non-root working directory
|
||||||
|
if os.path.exists(args.config):
|
||||||
|
args.config = os.path.abspath(args.config)
|
||||||
|
if args.command in {'run', 'try-repo'}:
|
||||||
|
args.files = [os.path.abspath(filename) for filename in args.files]
|
||||||
|
if args.commit_msg_filename is not None:
|
||||||
|
args.commit_msg_filename = os.path.abspath(
|
||||||
|
args.commit_msg_filename,
|
||||||
|
)
|
||||||
|
if args.command == 'try-repo' and os.path.exists(args.repo):
|
||||||
|
args.repo = os.path.abspath(args.repo)
|
||||||
|
|
||||||
|
toplevel = git.get_root()
|
||||||
|
os.chdir(toplevel)
|
||||||
|
|
||||||
|
args.config = os.path.relpath(args.config)
|
||||||
|
if args.command in {'run', 'try-repo'}:
|
||||||
|
args.files = [os.path.relpath(filename) for filename in args.files]
|
||||||
|
if args.commit_msg_filename is not None:
|
||||||
|
args.commit_msg_filename = os.path.relpath(
|
||||||
|
args.commit_msg_filename,
|
||||||
|
)
|
||||||
|
if args.command == 'try-repo' and os.path.exists(args.repo):
|
||||||
|
args.repo = os.path.relpath(args.repo)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
argv = argv if argv is not None else sys.argv[1:]
|
argv = argv if argv is not None else sys.argv[1:]
|
||||||
argv = [five.to_text(arg) for arg in argv]
|
parser = argparse.ArgumentParser(prog='pre-commit')
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
# https://stackoverflow.com/a/8521644/812183
|
# https://stackoverflow.com/a/8521644/812183
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-V', '--version',
|
'-V', '--version',
|
||||||
action='version',
|
action='version',
|
||||||
version='%(prog)s {}'.format(C.VERSION),
|
version=f'%(prog)s {C.VERSION}',
|
||||||
)
|
)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest='command')
|
subparsers = parser.add_subparsers(dest='command')
|
||||||
|
|
||||||
install_parser = subparsers.add_parser(
|
def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser:
|
||||||
'install', help='Install the pre-commit script.',
|
parser = subparsers.add_parser(name, help=help)
|
||||||
|
add_color_option(parser)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
autoupdate_parser = _add_cmd(
|
||||||
|
'autoupdate',
|
||||||
|
help="Auto-update pre-commit config to the latest repos' versions.",
|
||||||
)
|
)
|
||||||
_add_color_option(install_parser)
|
_add_config_option(autoupdate_parser)
|
||||||
|
autoupdate_parser.add_argument(
|
||||||
|
'--bleeding-edge', action='store_true',
|
||||||
|
help=(
|
||||||
|
'Update to the bleeding edge of `HEAD` instead of the latest '
|
||||||
|
'tagged version (the default behavior).'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
autoupdate_parser.add_argument(
|
||||||
|
'--freeze', action='store_true',
|
||||||
|
help='Store "frozen" hashes in `rev` instead of tag names',
|
||||||
|
)
|
||||||
|
autoupdate_parser.add_argument(
|
||||||
|
'--repo', dest='repos', action='append', metavar='REPO', default=[],
|
||||||
|
help='Only update this repository -- may be specified multiple times.',
|
||||||
|
)
|
||||||
|
autoupdate_parser.add_argument(
|
||||||
|
'-j', '--jobs', type=int, default=1,
|
||||||
|
help='Number of threads to use. (default %(default)s).',
|
||||||
|
)
|
||||||
|
|
||||||
|
_add_cmd('clean', help='Clean out pre-commit files.')
|
||||||
|
|
||||||
|
_add_cmd('gc', help='Clean unused cached repos.')
|
||||||
|
|
||||||
|
hazmat_parser = _add_cmd(
|
||||||
|
'hazmat', help='Composable tools for rare use in hook `entry`.',
|
||||||
|
)
|
||||||
|
hazmat.add_parsers(hazmat_parser)
|
||||||
|
|
||||||
|
init_templatedir_parser = _add_cmd(
|
||||||
|
'init-templatedir',
|
||||||
|
help=(
|
||||||
|
'Install hook script in a directory intended for use with '
|
||||||
|
'`git config init.templateDir`.'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_add_config_option(init_templatedir_parser)
|
||||||
|
init_templatedir_parser.add_argument(
|
||||||
|
'directory', help='The directory in which to write the hook script.',
|
||||||
|
)
|
||||||
|
init_templatedir_parser.add_argument(
|
||||||
|
'--no-allow-missing-config',
|
||||||
|
action='store_false',
|
||||||
|
dest='allow_missing_config',
|
||||||
|
help='Assume cloned repos should have a `pre-commit` config.',
|
||||||
|
)
|
||||||
|
_add_hook_type_option(init_templatedir_parser)
|
||||||
|
|
||||||
|
install_parser = _add_cmd('install', help='Install the pre-commit script.')
|
||||||
_add_config_option(install_parser)
|
_add_config_option(install_parser)
|
||||||
install_parser.add_argument(
|
install_parser.add_argument(
|
||||||
'-f', '--overwrite', action='store_true',
|
'-f', '--overwrite', action='store_true',
|
||||||
|
|
@ -121,14 +285,14 @@ def main(argv=None):
|
||||||
)
|
)
|
||||||
_add_hook_type_option(install_parser)
|
_add_hook_type_option(install_parser)
|
||||||
install_parser.add_argument(
|
install_parser.add_argument(
|
||||||
'--allow-missing-config', action='store_true', default=False,
|
'--allow-missing-config', action='store_true',
|
||||||
help=(
|
help=(
|
||||||
'Whether to allow a missing `pre-commit` configuration file '
|
'Whether to allow a missing `pre-commit` configuration file '
|
||||||
'or exit with a failure code.'
|
'or exit with a failure code.'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
install_hooks_parser = subparsers.add_parser(
|
install_hooks_parser = _add_cmd(
|
||||||
'install-hooks',
|
'install-hooks',
|
||||||
help=(
|
help=(
|
||||||
'Install hook environments for all environments in the config '
|
'Install hook environments for all environments in the config '
|
||||||
|
|
@ -136,65 +300,24 @@ def main(argv=None):
|
||||||
'useful.'
|
'useful.'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
_add_color_option(install_hooks_parser)
|
|
||||||
_add_config_option(install_hooks_parser)
|
_add_config_option(install_hooks_parser)
|
||||||
|
|
||||||
uninstall_parser = subparsers.add_parser(
|
migrate_config_parser = _add_cmd(
|
||||||
'uninstall', help='Uninstall the pre-commit script.',
|
|
||||||
)
|
|
||||||
_add_color_option(uninstall_parser)
|
|
||||||
_add_config_option(uninstall_parser)
|
|
||||||
_add_hook_type_option(uninstall_parser)
|
|
||||||
|
|
||||||
clean_parser = subparsers.add_parser(
|
|
||||||
'clean', help='Clean out pre-commit files.',
|
|
||||||
)
|
|
||||||
_add_color_option(clean_parser)
|
|
||||||
_add_config_option(clean_parser)
|
|
||||||
autoupdate_parser = subparsers.add_parser(
|
|
||||||
'autoupdate',
|
|
||||||
help="Auto-update pre-commit config to the latest repos' versions.",
|
|
||||||
)
|
|
||||||
_add_color_option(autoupdate_parser)
|
|
||||||
_add_config_option(autoupdate_parser)
|
|
||||||
autoupdate_parser.add_argument(
|
|
||||||
'--tags-only', action='store_true', help='LEGACY: for compatibility',
|
|
||||||
)
|
|
||||||
autoupdate_parser.add_argument(
|
|
||||||
'--bleeding-edge', action='store_true',
|
|
||||||
help=(
|
|
||||||
'Update to the bleeding edge of `master` instead of the latest '
|
|
||||||
'tagged version (the default behavior).'
|
|
||||||
),
|
|
||||||
)
|
|
||||||
autoupdate_parser.add_argument(
|
|
||||||
'--repo', dest='repos', action='append', metavar='REPO',
|
|
||||||
help='Only update this repository -- may be specified multiple times.',
|
|
||||||
)
|
|
||||||
|
|
||||||
migrate_config_parser = subparsers.add_parser(
|
|
||||||
'migrate-config',
|
'migrate-config',
|
||||||
help='Migrate list configuration to new map configuration.',
|
help='Migrate list configuration to new map configuration.',
|
||||||
)
|
)
|
||||||
_add_color_option(migrate_config_parser)
|
|
||||||
_add_config_option(migrate_config_parser)
|
_add_config_option(migrate_config_parser)
|
||||||
|
|
||||||
run_parser = subparsers.add_parser('run', help='Run hooks.')
|
run_parser = _add_cmd('run', help='Run hooks.')
|
||||||
_add_color_option(run_parser)
|
|
||||||
_add_config_option(run_parser)
|
_add_config_option(run_parser)
|
||||||
_add_run_options(run_parser)
|
_add_run_options(run_parser)
|
||||||
|
|
||||||
sample_config_parser = subparsers.add_parser(
|
_add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file')
|
||||||
'sample-config', help='Produce a sample {} file'.format(C.CONFIG_FILE),
|
|
||||||
)
|
|
||||||
_add_color_option(sample_config_parser)
|
|
||||||
_add_config_option(sample_config_parser)
|
|
||||||
|
|
||||||
try_repo_parser = subparsers.add_parser(
|
try_repo_parser = _add_cmd(
|
||||||
'try-repo',
|
'try-repo',
|
||||||
help='Try the hooks in a repository, useful for developing new hooks.',
|
help='Try the hooks in a repository, useful for developing new hooks.',
|
||||||
)
|
)
|
||||||
_add_color_option(try_repo_parser)
|
|
||||||
_add_config_option(try_repo_parser)
|
_add_config_option(try_repo_parser)
|
||||||
try_repo_parser.add_argument(
|
try_repo_parser.add_argument(
|
||||||
'repo', help='Repository to source hooks from.',
|
'repo', help='Repository to source hooks from.',
|
||||||
|
|
@ -208,11 +331,39 @@ def main(argv=None):
|
||||||
)
|
)
|
||||||
_add_run_options(try_repo_parser)
|
_add_run_options(try_repo_parser)
|
||||||
|
|
||||||
|
uninstall_parser = _add_cmd(
|
||||||
|
'uninstall', help='Uninstall the pre-commit script.',
|
||||||
|
)
|
||||||
|
_add_config_option(uninstall_parser)
|
||||||
|
_add_hook_type_option(uninstall_parser)
|
||||||
|
|
||||||
|
validate_config_parser = _add_cmd(
|
||||||
|
'validate-config', help='Validate .pre-commit-config.yaml files',
|
||||||
|
)
|
||||||
|
validate_config_parser.add_argument('filenames', nargs='*')
|
||||||
|
|
||||||
|
validate_manifest_parser = _add_cmd(
|
||||||
|
'validate-manifest', help='Validate .pre-commit-hooks.yaml files',
|
||||||
|
)
|
||||||
|
validate_manifest_parser.add_argument('filenames', nargs='*')
|
||||||
|
|
||||||
|
# does not use `_add_cmd` because it doesn't use `--color`
|
||||||
help = subparsers.add_parser(
|
help = subparsers.add_parser(
|
||||||
'help', help='Show help for a specific command.',
|
'help', help='Show help for a specific command.',
|
||||||
)
|
)
|
||||||
help.add_argument('help_cmd', nargs='?', help='Command to show help for.')
|
help.add_argument('help_cmd', nargs='?', help='Command to show help for.')
|
||||||
|
|
||||||
|
# not intended for users to call this directly
|
||||||
|
hook_impl_parser = subparsers.add_parser('hook-impl')
|
||||||
|
add_color_option(hook_impl_parser)
|
||||||
|
_add_config_option(hook_impl_parser)
|
||||||
|
hook_impl_parser.add_argument('--hook-type')
|
||||||
|
hook_impl_parser.add_argument('--hook-dir')
|
||||||
|
hook_impl_parser.add_argument(
|
||||||
|
'--skip-on-missing-config', action='store_true',
|
||||||
|
)
|
||||||
|
hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
# argparse doesn't really provide a way to use a `default` subparser
|
# argparse doesn't really provide a way to use a `default` subparser
|
||||||
if len(argv) == 0:
|
if len(argv) == 0:
|
||||||
argv = ['run']
|
argv = ['run']
|
||||||
|
|
@ -222,56 +373,82 @@ def main(argv=None):
|
||||||
parser.parse_args([args.help_cmd, '--help'])
|
parser.parse_args([args.help_cmd, '--help'])
|
||||||
elif args.command == 'help':
|
elif args.command == 'help':
|
||||||
parser.parse_args(['--help'])
|
parser.parse_args(['--help'])
|
||||||
elif args.command in {'run', 'try-repo'}:
|
|
||||||
args.files = [
|
|
||||||
os.path.relpath(os.path.abspath(filename), git.get_root())
|
|
||||||
for filename in args.files
|
|
||||||
]
|
|
||||||
|
|
||||||
with error_handler():
|
with error_handler(), logging_handler(args.color):
|
||||||
add_logging_handler(args.color)
|
|
||||||
runner = Runner.create(args.config)
|
|
||||||
store = Store()
|
|
||||||
git.check_for_cygwin_mismatch()
|
git.check_for_cygwin_mismatch()
|
||||||
|
|
||||||
if args.command == 'install':
|
store = Store()
|
||||||
return install(
|
|
||||||
runner, store,
|
if args.command not in COMMANDS_NO_GIT:
|
||||||
overwrite=args.overwrite, hooks=args.install_hooks,
|
_adjust_args_and_chdir(args)
|
||||||
hook_type=args.hook_type,
|
store.mark_config_used(args.config)
|
||||||
skip_on_missing_conf=args.allow_missing_config,
|
|
||||||
|
if args.command == 'autoupdate':
|
||||||
|
return autoupdate(
|
||||||
|
args.config,
|
||||||
|
tags_only=not args.bleeding_edge,
|
||||||
|
freeze=args.freeze,
|
||||||
|
repos=args.repos,
|
||||||
|
jobs=args.jobs,
|
||||||
)
|
)
|
||||||
elif args.command == 'install-hooks':
|
|
||||||
return install_hooks(runner, store)
|
|
||||||
elif args.command == 'uninstall':
|
|
||||||
return uninstall(runner, hook_type=args.hook_type)
|
|
||||||
elif args.command == 'clean':
|
elif args.command == 'clean':
|
||||||
return clean(store)
|
return clean(store)
|
||||||
elif args.command == 'autoupdate':
|
elif args.command == 'gc':
|
||||||
if args.tags_only:
|
return gc(store)
|
||||||
logger.warning('--tags-only is the default')
|
elif args.command == 'hazmat':
|
||||||
return autoupdate(
|
return hazmat.impl(args)
|
||||||
runner, store,
|
elif args.command == 'hook-impl':
|
||||||
tags_only=not args.bleeding_edge,
|
return hook_impl(
|
||||||
repos=args.repos,
|
store,
|
||||||
|
config=args.config,
|
||||||
|
color=args.color,
|
||||||
|
hook_type=args.hook_type,
|
||||||
|
hook_dir=args.hook_dir,
|
||||||
|
skip_on_missing_config=args.skip_on_missing_config,
|
||||||
|
args=args.rest[1:],
|
||||||
)
|
)
|
||||||
|
elif args.command == 'install':
|
||||||
|
return install(
|
||||||
|
args.config, store,
|
||||||
|
hook_types=args.hook_types,
|
||||||
|
overwrite=args.overwrite,
|
||||||
|
hooks=args.install_hooks,
|
||||||
|
skip_on_missing_config=args.allow_missing_config,
|
||||||
|
)
|
||||||
|
elif args.command == 'init-templatedir':
|
||||||
|
return init_templatedir(
|
||||||
|
args.config, store, args.directory,
|
||||||
|
hook_types=args.hook_types,
|
||||||
|
skip_on_missing_config=args.allow_missing_config,
|
||||||
|
)
|
||||||
|
elif args.command == 'install-hooks':
|
||||||
|
return install_hooks(args.config, store)
|
||||||
elif args.command == 'migrate-config':
|
elif args.command == 'migrate-config':
|
||||||
return migrate_config(runner)
|
return migrate_config(args.config)
|
||||||
elif args.command == 'run':
|
elif args.command == 'run':
|
||||||
return run(runner, store, args)
|
return run(args.config, store, args)
|
||||||
elif args.command == 'sample-config':
|
elif args.command == 'sample-config':
|
||||||
return sample_config()
|
return sample_config()
|
||||||
elif args.command == 'try-repo':
|
elif args.command == 'try-repo':
|
||||||
return try_repo(args)
|
return try_repo(args)
|
||||||
|
elif args.command == 'uninstall':
|
||||||
|
return uninstall(
|
||||||
|
config_file=args.config,
|
||||||
|
hook_types=args.hook_types,
|
||||||
|
)
|
||||||
|
elif args.command == 'validate-config':
|
||||||
|
return validate_config(args.filenames)
|
||||||
|
elif args.command == 'validate-manifest':
|
||||||
|
return validate_manifest(args.filenames)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
'Command {} not implemented.'.format(args.command),
|
f'Command {args.command} not implemented.',
|
||||||
)
|
)
|
||||||
|
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
'Command {} failed to exit with a returncode'.format(args.command),
|
f'Command {args.command} failed to exit with a returncode',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os.path
|
|
||||||
import tarfile
|
|
||||||
|
|
||||||
from pre_commit import output
|
|
||||||
from pre_commit.util import cmd_output
|
|
||||||
from pre_commit.util import resource_filename
|
|
||||||
from pre_commit.util import rmtree
|
|
||||||
from pre_commit.util import tmpdir
|
|
||||||
|
|
||||||
|
|
||||||
# This is a script for generating the tarred resources for git repo
|
|
||||||
# dependencies. Currently it's just for "vendoring" ruby support packages.
|
|
||||||
|
|
||||||
|
|
||||||
REPOS = (
|
|
||||||
('rbenv', 'git://github.com/rbenv/rbenv', 'e60ad4a'),
|
|
||||||
('ruby-build', 'git://github.com/rbenv/ruby-build', '9bc9971'),
|
|
||||||
(
|
|
||||||
'ruby-download',
|
|
||||||
'git://github.com/garnieretienne/rvm-download',
|
|
||||||
'09bd7c6',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_archive(name, repo, ref, destdir):
|
|
||||||
"""Makes an archive of a repository in the given destdir.
|
|
||||||
|
|
||||||
:param text name: Name to give the archive. For instance foo. The file
|
|
||||||
that is created will be called foo.tar.gz.
|
|
||||||
:param text repo: Repository to clone.
|
|
||||||
:param text ref: Tag/SHA/branch to check out.
|
|
||||||
:param text destdir: Directory to place archives in.
|
|
||||||
"""
|
|
||||||
output_path = os.path.join(destdir, name + '.tar.gz')
|
|
||||||
with tmpdir() as tempdir:
|
|
||||||
# Clone the repository to the temporary directory
|
|
||||||
cmd_output('git', 'clone', repo, tempdir)
|
|
||||||
cmd_output('git', 'checkout', ref, cwd=tempdir)
|
|
||||||
|
|
||||||
# We don't want the '.git' directory
|
|
||||||
# It adds a bunch of size to the archive and we don't use it at
|
|
||||||
# runtime
|
|
||||||
rmtree(os.path.join(tempdir, '.git'))
|
|
||||||
|
|
||||||
with tarfile.open(output_path, 'w|gz') as tf:
|
|
||||||
tf.add(tempdir, name)
|
|
||||||
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('--dest', default=resource_filename())
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
for archive_name, repo, ref in REPOS:
|
|
||||||
output.write_line('Making {}.tar.gz for {}@{}'.format(
|
|
||||||
archive_name, repo, ref,
|
|
||||||
))
|
|
||||||
make_archive(archive_name, repo, ref, args.dest)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
exit(main())
|
|
||||||
|
|
@ -1,34 +1,34 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
from pre_commit.clientlib import load_config
|
from pre_commit.clientlib import load_config
|
||||||
from pre_commit.commands.run import _filter_by_include_exclude
|
from pre_commit.commands.run import Classifier
|
||||||
from pre_commit.commands.run import _filter_by_types
|
from pre_commit.repository import all_hooks
|
||||||
from pre_commit.repository import repositories
|
|
||||||
from pre_commit.store import Store
|
from pre_commit.store import Store
|
||||||
|
|
||||||
|
|
||||||
def check_all_hooks_match_files(config_file):
|
def check_all_hooks_match_files(config_file: str) -> int:
|
||||||
files = git.get_all_files()
|
config = load_config(config_file)
|
||||||
|
classifier = Classifier.from_config(
|
||||||
|
git.get_all_files(), config['files'], config['exclude'],
|
||||||
|
)
|
||||||
retv = 0
|
retv = 0
|
||||||
|
|
||||||
for repo in repositories(load_config(config_file), Store()):
|
for hook in all_hooks(config, Store()):
|
||||||
for hook_id, hook in repo.hooks:
|
if hook.always_run or hook.language == 'fail':
|
||||||
if hook['always_run']:
|
continue
|
||||||
continue
|
elif not any(classifier.filenames_for_hook(hook)):
|
||||||
include, exclude = hook['files'], hook['exclude']
|
print(f'{hook.id} does not apply to this repository')
|
||||||
filtered = _filter_by_include_exclude(files, include, exclude)
|
retv = 1
|
||||||
types, exclude_types = hook['types'], hook['exclude_types']
|
|
||||||
filtered = _filter_by_types(filtered, types, exclude_types)
|
|
||||||
if not filtered:
|
|
||||||
print('{} does not apply to this repository'.format(hook_id))
|
|
||||||
retv = 1
|
|
||||||
|
|
||||||
return retv
|
return retv
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
@ -40,4 +40,4 @@ def main(argv=None):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from __future__ import print_function
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from cfgv import apply_defaults
|
from cfgv import apply_defaults
|
||||||
|
|
||||||
|
|
@ -9,9 +11,14 @@ import pre_commit.constants as C
|
||||||
from pre_commit import git
|
from pre_commit import git
|
||||||
from pre_commit.clientlib import load_config
|
from pre_commit.clientlib import load_config
|
||||||
from pre_commit.clientlib import MANIFEST_HOOK_DICT
|
from pre_commit.clientlib import MANIFEST_HOOK_DICT
|
||||||
|
from pre_commit.commands.run import Classifier
|
||||||
|
|
||||||
|
|
||||||
def exclude_matches_any(filenames, include, exclude):
|
def exclude_matches_any(
|
||||||
|
filenames: Iterable[str],
|
||||||
|
include: str,
|
||||||
|
exclude: str,
|
||||||
|
) -> bool:
|
||||||
if exclude == '^$':
|
if exclude == '^$':
|
||||||
return True
|
return True
|
||||||
include_re, exclude_re = re.compile(include), re.compile(exclude)
|
include_re, exclude_re = re.compile(include), re.compile(exclude)
|
||||||
|
|
@ -21,36 +28,47 @@ def exclude_matches_any(filenames, include, exclude):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_useless_excludes(config_file):
|
def check_useless_excludes(config_file: str) -> int:
|
||||||
config = load_config(config_file)
|
config = load_config(config_file)
|
||||||
files = git.get_all_files()
|
filenames = git.get_all_files()
|
||||||
|
classifier = Classifier.from_config(
|
||||||
|
filenames, config['files'], config['exclude'],
|
||||||
|
)
|
||||||
retv = 0
|
retv = 0
|
||||||
|
|
||||||
exclude = config['exclude']
|
exclude = config['exclude']
|
||||||
if not exclude_matches_any(files, '', exclude):
|
if not exclude_matches_any(filenames, '', exclude):
|
||||||
print(
|
print(
|
||||||
'The global exclude pattern {!r} does not match any files'
|
f'The global exclude pattern {exclude!r} does not match any files',
|
||||||
.format(exclude),
|
|
||||||
)
|
)
|
||||||
retv = 1
|
retv = 1
|
||||||
|
|
||||||
for repo in config['repos']:
|
for repo in config['repos']:
|
||||||
for hook in repo['hooks']:
|
for hook in repo['hooks']:
|
||||||
|
# the default of manifest hooks is `types: [file]` but we may
|
||||||
|
# be configuring a symlink hook while there's a broken symlink
|
||||||
|
hook.setdefault('types', [])
|
||||||
# Not actually a manifest dict, but this more accurately reflects
|
# Not actually a manifest dict, but this more accurately reflects
|
||||||
# the defaults applied during runtime
|
# the defaults applied during runtime
|
||||||
hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
|
hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
|
||||||
|
names = classifier.by_types(
|
||||||
|
classifier.filenames,
|
||||||
|
hook['types'],
|
||||||
|
hook['types_or'],
|
||||||
|
hook['exclude_types'],
|
||||||
|
)
|
||||||
include, exclude = hook['files'], hook['exclude']
|
include, exclude = hook['files'], hook['exclude']
|
||||||
if not exclude_matches_any(files, include, exclude):
|
if not exclude_matches_any(names, include, exclude):
|
||||||
print(
|
print(
|
||||||
'The exclude pattern {!r} for {} does not match any files'
|
f'The exclude pattern {exclude!r} for {hook["id"]} does '
|
||||||
.format(exclude, hook['id']),
|
f'not match any files',
|
||||||
)
|
)
|
||||||
retv = 1
|
retv = 1
|
||||||
|
|
||||||
return retv
|
return retv
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
@ -62,4 +80,4 @@ def main(argv=None):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|
|
||||||
17
pre_commit/meta_hooks/identity.py
Normal file
17
pre_commit/meta_hooks/identity.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from pre_commit import output
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
|
argv = argv if argv is not None else sys.argv[1:]
|
||||||
|
for arg in argv:
|
||||||
|
output.write_line(arg)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
raise SystemExit(main())
|
||||||
|
|
@ -1,88 +1,33 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any
|
||||||
from pre_commit import color
|
from typing import IO
|
||||||
from pre_commit import five
|
|
||||||
from pre_commit.util import noop_context
|
|
||||||
|
|
||||||
|
|
||||||
def get_hook_message(
|
def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
|
||||||
start,
|
stream.write(s.encode())
|
||||||
postfix='',
|
|
||||||
end_msg=None,
|
|
||||||
end_len=0,
|
|
||||||
end_color=None,
|
|
||||||
use_color=None,
|
|
||||||
cols=80,
|
|
||||||
):
|
|
||||||
"""Prints a message for running a hook.
|
|
||||||
|
|
||||||
This currently supports three approaches:
|
|
||||||
|
|
||||||
# Print `start` followed by dots, leaving 6 characters at the end
|
|
||||||
>>> print_hook_message('start', end_len=6)
|
|
||||||
start...............................................................
|
|
||||||
|
|
||||||
# Print `start` followed by dots with the end message colored if coloring
|
|
||||||
# is specified and a newline afterwards
|
|
||||||
>>> print_hook_message(
|
|
||||||
'start',
|
|
||||||
end_msg='end',
|
|
||||||
end_color=color.RED,
|
|
||||||
use_color=True,
|
|
||||||
)
|
|
||||||
start...................................................................end
|
|
||||||
|
|
||||||
# Print `start` followed by dots, followed by the `postfix` message
|
|
||||||
# uncolored, followed by the `end_msg` colored if specified and a newline
|
|
||||||
# afterwards
|
|
||||||
>>> print_hook_message(
|
|
||||||
'start',
|
|
||||||
postfix='postfix ',
|
|
||||||
end_msg='end',
|
|
||||||
end_color=color.RED,
|
|
||||||
use_color=True,
|
|
||||||
)
|
|
||||||
start...........................................................postfix end
|
|
||||||
"""
|
|
||||||
if bool(end_msg) == bool(end_len):
|
|
||||||
raise ValueError('Expected one of (`end_msg`, `end_len`)')
|
|
||||||
if end_msg is not None and (end_color is None or use_color is None):
|
|
||||||
raise ValueError(
|
|
||||||
'`end_color` and `use_color` are required with `end_msg`',
|
|
||||||
)
|
|
||||||
|
|
||||||
if end_len:
|
|
||||||
return start + '.' * (cols - len(start) - end_len - 1)
|
|
||||||
else:
|
|
||||||
return '{}{}{}{}\n'.format(
|
|
||||||
start,
|
|
||||||
'.' * (cols - len(start) - len(postfix) - len(end_msg) - 1),
|
|
||||||
postfix,
|
|
||||||
color.format_color(end_msg, end_color, use_color),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
stdout_byte_stream = getattr(sys.stdout, 'buffer', sys.stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def write(s, stream=stdout_byte_stream):
|
|
||||||
stream.write(five.to_bytes(s))
|
|
||||||
stream.flush()
|
stream.flush()
|
||||||
|
|
||||||
|
|
||||||
def write_line(s=None, stream=stdout_byte_stream, logfile_name=None):
|
def write_line_b(
|
||||||
output_streams = [stream]
|
s: bytes | None = None,
|
||||||
if logfile_name:
|
stream: IO[bytes] = sys.stdout.buffer,
|
||||||
ctx = open(logfile_name, 'ab')
|
logfile_name: str | None = None,
|
||||||
output_streams.append(ctx)
|
) -> None:
|
||||||
else:
|
with contextlib.ExitStack() as exit_stack:
|
||||||
ctx = noop_context()
|
output_streams = [stream]
|
||||||
|
if logfile_name:
|
||||||
|
stream = exit_stack.enter_context(open(logfile_name, 'ab'))
|
||||||
|
output_streams.append(stream)
|
||||||
|
|
||||||
with ctx:
|
|
||||||
for output_stream in output_streams:
|
for output_stream in output_streams:
|
||||||
if s is not None:
|
if s is not None:
|
||||||
output_stream.write(five.to_bytes(s))
|
output_stream.write(s)
|
||||||
output_stream.write(b'\n')
|
output_stream.write(b'\n')
|
||||||
output_stream.flush()
|
output_stream.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def write_line(s: str | None = None, **kwargs: Any) -> None:
|
||||||
|
write_line_b(s.encode() if s is not None else s, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,36 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import annotations
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import NoReturn
|
||||||
|
|
||||||
from identify.identify import parse_shebang_from_file
|
from identify.identify import parse_shebang_from_file
|
||||||
|
|
||||||
|
|
||||||
class ExecutableNotFoundError(OSError):
|
class ExecutableNotFoundError(OSError):
|
||||||
def to_output(self):
|
def to_output(self) -> tuple[int, bytes, None]:
|
||||||
return (1, self.args[0].encode('UTF-8'), b'')
|
return (1, self.args[0].encode(), None)
|
||||||
|
|
||||||
|
|
||||||
def parse_filename(filename):
|
def parse_filename(filename: str) -> tuple[str, ...]:
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return ()
|
return ()
|
||||||
else:
|
else:
|
||||||
return parse_shebang_from_file(filename)
|
return parse_shebang_from_file(filename)
|
||||||
|
|
||||||
|
|
||||||
def find_executable(exe, _environ=None):
|
def find_executable(
|
||||||
|
exe: str, *, env: Mapping[str, str] | None = None,
|
||||||
|
) -> str | None:
|
||||||
exe = os.path.normpath(exe)
|
exe = os.path.normpath(exe)
|
||||||
if os.sep in exe:
|
if os.sep in exe:
|
||||||
return exe
|
return exe
|
||||||
|
|
||||||
environ = _environ if _environ is not None else os.environ
|
environ = env if env is not None else os.environ
|
||||||
|
|
||||||
if 'PATHEXT' in environ:
|
if 'PATHEXT' in environ:
|
||||||
possible_exe_names = tuple(
|
exts = environ['PATHEXT'].split(os.pathsep)
|
||||||
exe + ext.lower() for ext in environ['PATHEXT'].split(os.pathsep)
|
possible_exe_names = tuple(f'{exe}{ext}' for ext in exts) + (exe,)
|
||||||
) + (exe,)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
possible_exe_names = (exe,)
|
possible_exe_names = (exe,)
|
||||||
|
|
||||||
|
|
@ -42,24 +43,30 @@ def find_executable(exe, _environ=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def normexe(orig):
|
def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str:
|
||||||
def _error(msg):
|
def _error(msg: str) -> NoReturn:
|
||||||
raise ExecutableNotFoundError('Executable `{}` {}'.format(orig, msg))
|
raise ExecutableNotFoundError(f'Executable `{orig}` {msg}')
|
||||||
|
|
||||||
if os.sep not in orig and (not os.altsep or os.altsep not in orig):
|
if os.sep not in orig and (not os.altsep or os.altsep not in orig):
|
||||||
exe = find_executable(orig)
|
exe = find_executable(orig, env=env)
|
||||||
if exe is None:
|
if exe is None:
|
||||||
_error('not found')
|
_error('not found')
|
||||||
return exe
|
return exe
|
||||||
elif not os.access(orig, os.X_OK):
|
|
||||||
_error('not found')
|
|
||||||
elif os.path.isdir(orig):
|
elif os.path.isdir(orig):
|
||||||
_error('is a directory')
|
_error('is a directory')
|
||||||
|
elif not os.path.isfile(orig):
|
||||||
|
_error('not found')
|
||||||
|
elif not os.access(orig, os.X_OK): # pragma: win32 no cover
|
||||||
|
_error('is not executable')
|
||||||
else:
|
else:
|
||||||
return orig
|
return orig
|
||||||
|
|
||||||
|
|
||||||
def normalize_cmd(cmd):
|
def normalize_cmd(
|
||||||
|
cmd: tuple[str, ...],
|
||||||
|
*,
|
||||||
|
env: Mapping[str, str] | None = None,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
"""Fixes for the following issues on windows
|
"""Fixes for the following issues on windows
|
||||||
- https://bugs.python.org/issue8557
|
- https://bugs.python.org/issue8557
|
||||||
- windows does not parse shebangs
|
- windows does not parse shebangs
|
||||||
|
|
@ -67,12 +74,12 @@ def normalize_cmd(cmd):
|
||||||
This function also makes deep-path shebangs work just fine
|
This function also makes deep-path shebangs work just fine
|
||||||
"""
|
"""
|
||||||
# Use PATH to determine the executable
|
# Use PATH to determine the executable
|
||||||
exe = normexe(cmd[0])
|
exe = normexe(cmd[0], env=env)
|
||||||
|
|
||||||
# Figure out the shebang from the resulting command
|
# Figure out the shebang from the resulting command
|
||||||
cmd = parse_filename(exe) + (exe,) + cmd[1:]
|
cmd = parse_filename(exe) + (exe,) + cmd[1:]
|
||||||
|
|
||||||
# This could have given us back another bare executable
|
# This could have given us back another bare executable
|
||||||
exe = normexe(cmd[0])
|
exe = normexe(cmd[0], env=env)
|
||||||
|
|
||||||
return (exe,) + cmd[1:]
|
return (exe,) + cmd[1:]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
class Prefix(object):
|
class Prefix(NamedTuple):
|
||||||
def __init__(self, prefix_dir):
|
prefix_dir: str
|
||||||
self.prefix_dir = prefix_dir
|
|
||||||
|
|
||||||
def path(self, *parts):
|
def path(self, *parts: str) -> str:
|
||||||
return os.path.normpath(os.path.join(self.prefix_dir, *parts))
|
return os.path.normpath(os.path.join(self.prefix_dir, *parts))
|
||||||
|
|
||||||
def exists(self, *parts):
|
def exists(self, *parts: str) -> bool:
|
||||||
return os.path.exists(self.path(*parts))
|
return os.path.exists(self.path(*parts))
|
||||||
|
|
||||||
def star(self, end):
|
def star(self, end: str) -> tuple[str, ...]:
|
||||||
paths = os.listdir(self.prefix_dir)
|
paths = os.listdir(self.prefix_dir)
|
||||||
return tuple(path for path in paths if path.endswith(end))
|
return tuple(path for path in paths if path.endswith(end))
|
||||||
|
|
|
||||||
|
|
@ -1,288 +1,237 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pipes
|
from collections.abc import Sequence
|
||||||
import shutil
|
from typing import Any
|
||||||
import sys
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
from cached_property import cached_property
|
|
||||||
from cfgv import apply_defaults
|
|
||||||
from cfgv import validate
|
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
from pre_commit import five
|
from pre_commit.all_languages import languages
|
||||||
from pre_commit import git
|
|
||||||
from pre_commit.clientlib import is_local_repo
|
|
||||||
from pre_commit.clientlib import is_meta_repo
|
|
||||||
from pre_commit.clientlib import load_manifest
|
from pre_commit.clientlib import load_manifest
|
||||||
from pre_commit.clientlib import MANIFEST_HOOK_DICT
|
from pre_commit.clientlib import LOCAL
|
||||||
from pre_commit.languages.all import languages
|
from pre_commit.clientlib import META
|
||||||
from pre_commit.languages.helpers import environment_dir
|
from pre_commit.hook import Hook
|
||||||
|
from pre_commit.lang_base import environment_dir
|
||||||
from pre_commit.prefix import Prefix
|
from pre_commit.prefix import Prefix
|
||||||
|
from pre_commit.store import Store
|
||||||
|
from pre_commit.util import clean_path_on_failure
|
||||||
|
from pre_commit.util import rmtree
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('pre_commit')
|
logger = logging.getLogger('pre_commit')
|
||||||
|
|
||||||
|
|
||||||
def _state(additional_deps):
|
def _state_filename_v1(venv: str) -> str:
|
||||||
return {'additional_dependencies': sorted(additional_deps)}
|
return os.path.join(venv, '.install_state_v1')
|
||||||
|
|
||||||
|
|
||||||
def _state_filename(prefix, venv):
|
def _state_filename_v2(venv: str) -> str:
|
||||||
return prefix.path(
|
return os.path.join(venv, '.install_state_v2')
|
||||||
venv, '.install_state_v' + C.INSTALLED_STATE_VERSION,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _read_state(prefix, venv):
|
def _state(additional_deps: Sequence[str]) -> object:
|
||||||
filename = _state_filename(prefix, venv)
|
return {'additional_dependencies': additional_deps}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_state(venv: str) -> object | None:
|
||||||
|
filename = _state_filename_v1(venv)
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return json.loads(io.open(filename).read())
|
with open(filename) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def _write_state(prefix, venv, state):
|
def _hook_installed(hook: Hook) -> bool:
|
||||||
state_filename = _state_filename(prefix, venv)
|
lang = languages[hook.language]
|
||||||
staging = state_filename + 'staging'
|
if lang.ENVIRONMENT_DIR is None:
|
||||||
with io.open(staging, 'w') as state_file:
|
return True
|
||||||
state_file.write(five.to_text(json.dumps(state)))
|
|
||||||
# Move the file into place atomically to indicate we've installed
|
|
||||||
os.rename(staging, state_filename)
|
|
||||||
|
|
||||||
|
venv = environment_dir(
|
||||||
def _installed(prefix, language_name, language_version, additional_deps):
|
hook.prefix,
|
||||||
language = languages[language_name]
|
lang.ENVIRONMENT_DIR,
|
||||||
venv = environment_dir(language.ENVIRONMENT_DIR, language_version)
|
hook.language_version,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
venv is None or (
|
(
|
||||||
_read_state(prefix, venv) == _state(additional_deps) and
|
os.path.exists(_state_filename_v2(venv)) or
|
||||||
language.healthy(prefix, language_version)
|
_read_state(venv) == _state(hook.additional_dependencies)
|
||||||
)
|
) and
|
||||||
|
not lang.health_check(hook.prefix, hook.language_version)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _install_all(venvs, repo_url, store):
|
def _hook_install(hook: Hook) -> None:
|
||||||
"""Tuple of (prefix, language, version, deps)"""
|
logger.info(f'Installing environment for {hook.src}.')
|
||||||
def _need_installed():
|
logger.info('Once installed this environment will be reused.')
|
||||||
return tuple(
|
logger.info('This may take a few minutes...')
|
||||||
(prefix, language_name, version, deps)
|
|
||||||
for prefix, language_name, version, deps in venvs
|
lang = languages[hook.language]
|
||||||
if not _installed(prefix, language_name, version, deps)
|
assert lang.ENVIRONMENT_DIR is not None
|
||||||
|
|
||||||
|
venv = environment_dir(
|
||||||
|
hook.prefix,
|
||||||
|
lang.ENVIRONMENT_DIR,
|
||||||
|
hook.language_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
# There's potentially incomplete cleanup from previous runs
|
||||||
|
# Clean it up!
|
||||||
|
if os.path.exists(venv):
|
||||||
|
rmtree(venv)
|
||||||
|
|
||||||
|
with clean_path_on_failure(venv):
|
||||||
|
lang.install_environment(
|
||||||
|
hook.prefix, hook.language_version, hook.additional_dependencies,
|
||||||
)
|
)
|
||||||
|
health_error = lang.health_check(hook.prefix, hook.language_version)
|
||||||
|
if health_error:
|
||||||
|
raise AssertionError(
|
||||||
|
f'BUG: expected environment for {hook.language} to be healthy '
|
||||||
|
f'immediately after install, please open an issue describing '
|
||||||
|
f'your environment\n\n'
|
||||||
|
f'more info:\n\n{health_error}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: remove v1 state writing, no longer needed after pre-commit 3.0
|
||||||
|
# Write our state to indicate we're installed
|
||||||
|
state_filename = _state_filename_v1(venv)
|
||||||
|
staging = f'{state_filename}staging'
|
||||||
|
with open(staging, 'w') as state_file:
|
||||||
|
state_file.write(json.dumps(_state(hook.additional_dependencies)))
|
||||||
|
# Move the file into place atomically to indicate we've installed
|
||||||
|
os.replace(staging, state_filename)
|
||||||
|
|
||||||
|
open(_state_filename_v2(venv), 'a+').close()
|
||||||
|
|
||||||
|
|
||||||
|
def _hook(
|
||||||
|
*hook_dicts: dict[str, Any],
|
||||||
|
root_config: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
ret, rest = dict(hook_dicts[0]), hook_dicts[1:]
|
||||||
|
for dct in rest:
|
||||||
|
ret.update(dct)
|
||||||
|
|
||||||
|
lang = ret['language']
|
||||||
|
if ret['language_version'] == C.DEFAULT:
|
||||||
|
ret['language_version'] = root_config['default_language_version'][lang]
|
||||||
|
if ret['language_version'] == C.DEFAULT:
|
||||||
|
ret['language_version'] = languages[lang].get_default_version()
|
||||||
|
|
||||||
|
if not ret['stages']:
|
||||||
|
ret['stages'] = root_config['default_stages']
|
||||||
|
|
||||||
|
if languages[lang].ENVIRONMENT_DIR is None:
|
||||||
|
if ret['language_version'] != C.DEFAULT:
|
||||||
|
logger.error(
|
||||||
|
f'The hook `{ret["id"]}` specifies `language_version` but is '
|
||||||
|
f'using language `{lang}` which does not install an '
|
||||||
|
f'environment. '
|
||||||
|
f'Perhaps you meant to use a specific language?',
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
if ret['additional_dependencies']:
|
||||||
|
logger.error(
|
||||||
|
f'The hook `{ret["id"]}` specifies `additional_dependencies` '
|
||||||
|
f'but is using language `{lang}` which does not install an '
|
||||||
|
f'environment. '
|
||||||
|
f'Perhaps you meant to use a specific language?',
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _non_cloned_repository_hooks(
|
||||||
|
repo_config: dict[str, Any],
|
||||||
|
store: Store,
|
||||||
|
root_config: dict[str, Any],
|
||||||
|
) -> tuple[Hook, ...]:
|
||||||
|
def _prefix(language_name: str, deps: Sequence[str]) -> Prefix:
|
||||||
|
language = languages[language_name]
|
||||||
|
# pygrep / script / system / docker_image do not have
|
||||||
|
# environments so they work out of the current directory
|
||||||
|
if language.ENVIRONMENT_DIR is None:
|
||||||
|
return Prefix(os.getcwd())
|
||||||
|
else:
|
||||||
|
return Prefix(store.make_local(deps))
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
Hook.create(
|
||||||
|
repo_config['repo'],
|
||||||
|
_prefix(hook['language'], hook['additional_dependencies']),
|
||||||
|
_hook(hook, root_config=root_config),
|
||||||
|
)
|
||||||
|
for hook in repo_config['hooks']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _cloned_repository_hooks(
|
||||||
|
repo_config: dict[str, Any],
|
||||||
|
store: Store,
|
||||||
|
root_config: dict[str, Any],
|
||||||
|
) -> tuple[Hook, ...]:
|
||||||
|
repo, rev = repo_config['repo'], repo_config['rev']
|
||||||
|
manifest_path = os.path.join(store.clone(repo, rev), C.MANIFEST_FILE)
|
||||||
|
by_id = {hook['id']: hook for hook in load_manifest(manifest_path)}
|
||||||
|
|
||||||
|
for hook in repo_config['hooks']:
|
||||||
|
if hook['id'] not in by_id:
|
||||||
|
logger.error(
|
||||||
|
f'`{hook["id"]}` is not present in repository {repo}. '
|
||||||
|
f'Typo? Perhaps it is introduced in a newer version? '
|
||||||
|
f'Often `pre-commit autoupdate` fixes this.',
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
hook_dcts = [
|
||||||
|
_hook(by_id[hook['id']], hook, root_config=root_config)
|
||||||
|
for hook in repo_config['hooks']
|
||||||
|
]
|
||||||
|
return tuple(
|
||||||
|
Hook.create(
|
||||||
|
repo_config['repo'],
|
||||||
|
Prefix(store.clone(repo, rev, hook['additional_dependencies'])),
|
||||||
|
hook,
|
||||||
|
)
|
||||||
|
for hook in hook_dcts
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _repository_hooks(
|
||||||
|
repo_config: dict[str, Any],
|
||||||
|
store: Store,
|
||||||
|
root_config: dict[str, Any],
|
||||||
|
) -> tuple[Hook, ...]:
|
||||||
|
if repo_config['repo'] in {LOCAL, META}:
|
||||||
|
return _non_cloned_repository_hooks(repo_config, store, root_config)
|
||||||
|
else:
|
||||||
|
return _cloned_repository_hooks(repo_config, store, root_config)
|
||||||
|
|
||||||
|
|
||||||
|
def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
|
||||||
|
def _need_installed() -> list[Hook]:
|
||||||
|
seen: set[tuple[Prefix, str, str, tuple[str, ...]]] = set()
|
||||||
|
ret = []
|
||||||
|
for hook in hooks:
|
||||||
|
if hook.install_key not in seen and not _hook_installed(hook):
|
||||||
|
ret.append(hook)
|
||||||
|
seen.add(hook.install_key)
|
||||||
|
return ret
|
||||||
|
|
||||||
if not _need_installed():
|
if not _need_installed():
|
||||||
return
|
return
|
||||||
with store.exclusive_lock():
|
with store.exclusive_lock():
|
||||||
# Another process may have already completed this work
|
# Another process may have already completed this work
|
||||||
need_installed = _need_installed()
|
for hook in _need_installed():
|
||||||
if not need_installed: # pragma: no cover (race)
|
_hook_install(hook)
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'Installing environment for {}.'.format(repo_url),
|
|
||||||
)
|
|
||||||
logger.info('Once installed this environment will be reused.')
|
|
||||||
logger.info('This may take a few minutes...')
|
|
||||||
|
|
||||||
for prefix, language_name, version, deps in need_installed:
|
|
||||||
language = languages[language_name]
|
|
||||||
venv = environment_dir(language.ENVIRONMENT_DIR, version)
|
|
||||||
|
|
||||||
# There's potentially incomplete cleanup from previous runs
|
|
||||||
# Clean it up!
|
|
||||||
if prefix.exists(venv):
|
|
||||||
shutil.rmtree(prefix.path(venv))
|
|
||||||
|
|
||||||
language.install_environment(prefix, version, deps)
|
|
||||||
# Write our state to indicate we're installed
|
|
||||||
state = _state(deps)
|
|
||||||
_write_state(prefix, venv, state)
|
|
||||||
|
|
||||||
|
|
||||||
def _hook(*hook_dicts):
|
def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]:
|
||||||
ret, rest = dict(hook_dicts[0]), hook_dicts[1:]
|
return tuple(
|
||||||
for dct in rest:
|
hook
|
||||||
ret.update(dct)
|
for repo in root_config['repos']
|
||||||
|
for hook in _repository_hooks(repo, store, root_config)
|
||||||
version = pkg_resources.parse_version(ret['minimum_pre_commit_version'])
|
)
|
||||||
if version > C.VERSION_PARSED:
|
|
||||||
logger.error(
|
|
||||||
'The hook `{}` requires pre-commit version {} but version {} '
|
|
||||||
'is installed. '
|
|
||||||
'Perhaps run `pip install --upgrade pre-commit`.'.format(
|
|
||||||
ret['id'], version, C.VERSION_PARSED,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if ret['language_version'] == 'default':
|
|
||||||
language = languages[ret['language']]
|
|
||||||
ret['language_version'] = language.get_default_version()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _hook_from_manifest_dct(dct):
|
|
||||||
dct = validate(apply_defaults(dct, MANIFEST_HOOK_DICT), MANIFEST_HOOK_DICT)
|
|
||||||
dct = _hook(dct)
|
|
||||||
return dct
|
|
||||||
|
|
||||||
|
|
||||||
class Repository(object):
|
|
||||||
def __init__(self, repo_config, store):
|
|
||||||
self.repo_config = repo_config
|
|
||||||
self.store = store
|
|
||||||
self.__installed = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create(cls, config, store):
|
|
||||||
if is_local_repo(config):
|
|
||||||
return LocalRepository(config, store)
|
|
||||||
elif is_meta_repo(config):
|
|
||||||
return MetaRepository(config, store)
|
|
||||||
else:
|
|
||||||
return cls(config, store)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def manifest_hooks(self):
|
|
||||||
repo, rev = self.repo_config['repo'], self.repo_config['rev']
|
|
||||||
repo_path = self.store.clone(repo, rev)
|
|
||||||
manifest_path = os.path.join(repo_path, C.MANIFEST_FILE)
|
|
||||||
return {hook['id']: hook for hook in load_manifest(manifest_path)}
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def hooks(self):
|
|
||||||
for hook in self.repo_config['hooks']:
|
|
||||||
if hook['id'] not in self.manifest_hooks:
|
|
||||||
logger.error(
|
|
||||||
'`{}` is not present in repository {}. '
|
|
||||||
'Typo? Perhaps it is introduced in a newer version? '
|
|
||||||
'Often `pre-commit autoupdate` fixes this.'.format(
|
|
||||||
hook['id'], self.repo_config['repo'],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return tuple(
|
|
||||||
(hook['id'], _hook(self.manifest_hooks[hook['id']], hook))
|
|
||||||
for hook in self.repo_config['hooks']
|
|
||||||
)
|
|
||||||
|
|
||||||
def _prefix_from_deps(self, language_name, deps):
|
|
||||||
repo, rev = self.repo_config['repo'], self.repo_config['rev']
|
|
||||||
return Prefix(self.store.clone(repo, rev, deps))
|
|
||||||
|
|
||||||
def _venvs(self):
|
|
||||||
ret = []
|
|
||||||
for _, hook in self.hooks:
|
|
||||||
language = hook['language']
|
|
||||||
version = hook['language_version']
|
|
||||||
deps = hook['additional_dependencies']
|
|
||||||
ret.append((
|
|
||||||
self._prefix_from_deps(language, deps),
|
|
||||||
language, version, deps,
|
|
||||||
))
|
|
||||||
return tuple(ret)
|
|
||||||
|
|
||||||
def require_installed(self):
|
|
||||||
if not self.__installed:
|
|
||||||
_install_all(self._venvs(), self.repo_config['repo'], self.store)
|
|
||||||
self.__installed = True
|
|
||||||
|
|
||||||
def run_hook(self, hook, file_args):
|
|
||||||
"""Run a hook.
|
|
||||||
|
|
||||||
:param dict hook:
|
|
||||||
:param tuple file_args: all the files to run the hook on
|
|
||||||
"""
|
|
||||||
self.require_installed()
|
|
||||||
language_name = hook['language']
|
|
||||||
deps = hook['additional_dependencies']
|
|
||||||
prefix = self._prefix_from_deps(language_name, deps)
|
|
||||||
return languages[language_name].run_hook(prefix, hook, file_args)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalRepository(Repository):
|
|
||||||
def _prefix_from_deps(self, language_name, deps):
|
|
||||||
"""local repositories have a prefix per hook"""
|
|
||||||
language = languages[language_name]
|
|
||||||
# pcre / pygrep / script / system / docker_image do not have
|
|
||||||
# environments so they work out of the current directory
|
|
||||||
if language.ENVIRONMENT_DIR is None:
|
|
||||||
return Prefix(git.get_root())
|
|
||||||
else:
|
|
||||||
return Prefix(self.store.make_local(deps))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def manifest(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def hooks(self):
|
|
||||||
return tuple(
|
|
||||||
(hook['id'], _hook_from_manifest_dct(hook))
|
|
||||||
for hook in self.repo_config['hooks']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MetaRepository(LocalRepository):
|
|
||||||
@cached_property
|
|
||||||
def manifest_hooks(self):
|
|
||||||
# The hooks are imported here to prevent circular imports.
|
|
||||||
from pre_commit.meta_hooks import check_hooks_apply
|
|
||||||
from pre_commit.meta_hooks import check_useless_excludes
|
|
||||||
|
|
||||||
def _make_entry(mod):
|
|
||||||
"""the hook `entry` is passed through `shlex.split()` by the
|
|
||||||
command runner, so to prevent issues with spaces and backslashes
|
|
||||||
(on Windows) it must be quoted here.
|
|
||||||
"""
|
|
||||||
return '{} -m {}'.format(pipes.quote(sys.executable), mod.__name__)
|
|
||||||
|
|
||||||
meta_hooks = [
|
|
||||||
{
|
|
||||||
'id': 'check-hooks-apply',
|
|
||||||
'name': 'Check hooks apply to the repository',
|
|
||||||
'files': C.CONFIG_FILE,
|
|
||||||
'language': 'system',
|
|
||||||
'entry': _make_entry(check_hooks_apply),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'check-useless-excludes',
|
|
||||||
'name': 'Check for useless excludes',
|
|
||||||
'files': C.CONFIG_FILE,
|
|
||||||
'language': 'system',
|
|
||||||
'entry': _make_entry(check_useless_excludes),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
hook['id']: _hook_from_manifest_dct(hook)
|
|
||||||
for hook in meta_hooks
|
|
||||||
}
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def hooks(self):
|
|
||||||
for hook in self.repo_config['hooks']:
|
|
||||||
if hook['id'] not in self.manifest_hooks:
|
|
||||||
logger.error(
|
|
||||||
'`{}` is not a valid meta hook. '
|
|
||||||
'Typo? Perhaps it is introduced in a newer version? '
|
|
||||||
'Often `pip install --upgrade pre-commit` fixes this.'
|
|
||||||
.format(hook['id']),
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return tuple(
|
|
||||||
(hook['id'], _hook(self.manifest_hooks[hook['id']], hook))
|
|
||||||
for hook in self.repo_config['hooks']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def repositories(config, store):
|
|
||||||
return tuple(Repository.create(x, store) for x in config['repos'])
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"name": "pre_commit_dummy_package",
|
|
||||||
"version": "0.0.0"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
Gem::Specification.new do |s|
|
|
||||||
s.name = 'pre_commit_dummy_package'
|
|
||||||
s.version = '0.0.0'
|
|
||||||
s.summary = 'dummy gem for pre-commit hooks'
|
|
||||||
s.authors = ['Anthony Sottile']
|
|
||||||
end
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
|
|
||||||
setup(name='pre-commit-dummy-package', version='0.0.0')
|
|
||||||
7
pre_commit/resources/empty_template_LICENSE.renv
Normal file
7
pre_commit/resources/empty_template_LICENSE.renv
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Copyright 2021 RStudio, PBC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
6
pre_commit/resources/empty_template_Makefile.PL
Normal file
6
pre_commit/resources/empty_template_Makefile.PL
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
use ExtUtils::MakeMaker;
|
||||||
|
|
||||||
|
WriteMakefile(
|
||||||
|
NAME => "PreCommitPlaceholder",
|
||||||
|
VERSION => "0.0.1",
|
||||||
|
);
|
||||||
440
pre_commit/resources/empty_template_activate.R
Normal file
440
pre_commit/resources/empty_template_activate.R
Normal file
|
|
@ -0,0 +1,440 @@
|
||||||
|
|
||||||
|
local({
|
||||||
|
|
||||||
|
# the requested version of renv
|
||||||
|
version <- "0.12.5"
|
||||||
|
|
||||||
|
# the project directory
|
||||||
|
project <- getwd()
|
||||||
|
|
||||||
|
# avoid recursion
|
||||||
|
if (!is.na(Sys.getenv("RENV_R_INITIALIZING", unset = NA)))
|
||||||
|
return(invisible(TRUE))
|
||||||
|
|
||||||
|
# signal that we're loading renv during R startup
|
||||||
|
Sys.setenv("RENV_R_INITIALIZING" = "true")
|
||||||
|
on.exit(Sys.unsetenv("RENV_R_INITIALIZING"), add = TRUE)
|
||||||
|
|
||||||
|
# signal that we've consented to use renv
|
||||||
|
options(renv.consent = TRUE)
|
||||||
|
|
||||||
|
# load the 'utils' package eagerly -- this ensures that renv shims, which
|
||||||
|
# mask 'utils' packages, will come first on the search path
|
||||||
|
library(utils, lib.loc = .Library)
|
||||||
|
|
||||||
|
# check to see if renv has already been loaded
|
||||||
|
if ("renv" %in% loadedNamespaces()) {
|
||||||
|
|
||||||
|
# if renv has already been loaded, and it's the requested version of renv,
|
||||||
|
# nothing to do
|
||||||
|
spec <- .getNamespaceInfo(.getNamespace("renv"), "spec")
|
||||||
|
if (identical(spec[["version"]], version))
|
||||||
|
return(invisible(TRUE))
|
||||||
|
|
||||||
|
# otherwise, unload and attempt to load the correct version of renv
|
||||||
|
unloadNamespace("renv")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# load bootstrap tools
|
||||||
|
bootstrap <- function(version, library) {
|
||||||
|
|
||||||
|
# attempt to download renv
|
||||||
|
tarball <- tryCatch(renv_bootstrap_download(version), error = identity)
|
||||||
|
if (inherits(tarball, "error"))
|
||||||
|
stop("failed to download renv ", version)
|
||||||
|
|
||||||
|
# now attempt to install
|
||||||
|
status <- tryCatch(renv_bootstrap_install(version, tarball, library), error = identity)
|
||||||
|
if (inherits(status, "error"))
|
||||||
|
stop("failed to install renv ", version)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_tests_running <- function() {
|
||||||
|
getOption("renv.tests.running", default = FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_repos <- function() {
|
||||||
|
|
||||||
|
# check for repos override
|
||||||
|
repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA)
|
||||||
|
if (!is.na(repos))
|
||||||
|
return(repos)
|
||||||
|
|
||||||
|
# if we're testing, re-use the test repositories
|
||||||
|
if (renv_bootstrap_tests_running())
|
||||||
|
return(getOption("renv.tests.repos"))
|
||||||
|
|
||||||
|
# retrieve current repos
|
||||||
|
repos <- getOption("repos")
|
||||||
|
|
||||||
|
# ensure @CRAN@ entries are resolved
|
||||||
|
repos[repos == "@CRAN@"] <- "https://cloud.r-project.org"
|
||||||
|
|
||||||
|
# add in renv.bootstrap.repos if set
|
||||||
|
default <- c(CRAN = "https://cloud.r-project.org")
|
||||||
|
extra <- getOption("renv.bootstrap.repos", default = default)
|
||||||
|
repos <- c(repos, extra)
|
||||||
|
|
||||||
|
# remove duplicates that might've snuck in
|
||||||
|
dupes <- duplicated(repos) | duplicated(names(repos))
|
||||||
|
repos[!dupes]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download <- function(version) {
|
||||||
|
|
||||||
|
# if the renv version number has 4 components, assume it must
|
||||||
|
# be retrieved via github
|
||||||
|
nv <- numeric_version(version)
|
||||||
|
components <- unclass(nv)[[1]]
|
||||||
|
|
||||||
|
methods <- if (length(components) == 4L) {
|
||||||
|
list(
|
||||||
|
renv_bootstrap_download_github
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
list(
|
||||||
|
renv_bootstrap_download_cran_latest,
|
||||||
|
renv_bootstrap_download_cran_archive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (method in methods) {
|
||||||
|
path <- tryCatch(method(version), error = identity)
|
||||||
|
if (is.character(path) && file.exists(path))
|
||||||
|
return(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop("failed to download renv ", version)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download_impl <- function(url, destfile) {
|
||||||
|
|
||||||
|
mode <- "wb"
|
||||||
|
|
||||||
|
# https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715
|
||||||
|
fixup <-
|
||||||
|
Sys.info()[["sysname"]] == "Windows" &&
|
||||||
|
substring(url, 1L, 5L) == "file:"
|
||||||
|
|
||||||
|
if (fixup)
|
||||||
|
mode <- "w+b"
|
||||||
|
|
||||||
|
utils::download.file(
|
||||||
|
url = url,
|
||||||
|
destfile = destfile,
|
||||||
|
mode = mode,
|
||||||
|
quiet = TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download_cran_latest <- function(version) {
|
||||||
|
|
||||||
|
repos <- renv_bootstrap_download_cran_latest_find(version)
|
||||||
|
|
||||||
|
message("* Downloading renv ", version, " from CRAN ... ", appendLF = FALSE)
|
||||||
|
|
||||||
|
info <- tryCatch(
|
||||||
|
utils::download.packages(
|
||||||
|
pkgs = "renv",
|
||||||
|
repos = repos,
|
||||||
|
destdir = tempdir(),
|
||||||
|
quiet = TRUE
|
||||||
|
),
|
||||||
|
condition = identity
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inherits(info, "condition")) {
|
||||||
|
message("FAILED")
|
||||||
|
return(FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
message("OK")
|
||||||
|
info[1, 2]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download_cran_latest_find <- function(version) {
|
||||||
|
|
||||||
|
all <- renv_bootstrap_repos()
|
||||||
|
|
||||||
|
for (repos in all) {
|
||||||
|
|
||||||
|
db <- tryCatch(
|
||||||
|
as.data.frame(
|
||||||
|
x = utils::available.packages(repos = repos),
|
||||||
|
stringsAsFactors = FALSE
|
||||||
|
),
|
||||||
|
error = identity
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inherits(db, "error"))
|
||||||
|
next
|
||||||
|
|
||||||
|
entry <- db[db$Package %in% "renv" & db$Version %in% version, ]
|
||||||
|
if (nrow(entry) == 0)
|
||||||
|
next
|
||||||
|
|
||||||
|
return(repos)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt <- "renv %s is not available from your declared package repositories"
|
||||||
|
stop(sprintf(fmt, version))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download_cran_archive <- function(version) {
|
||||||
|
|
||||||
|
name <- sprintf("renv_%s.tar.gz", version)
|
||||||
|
repos <- renv_bootstrap_repos()
|
||||||
|
urls <- file.path(repos, "src/contrib/Archive/renv", name)
|
||||||
|
destfile <- file.path(tempdir(), name)
|
||||||
|
|
||||||
|
message("* Downloading renv ", version, " from CRAN archive ... ", appendLF = FALSE)
|
||||||
|
|
||||||
|
for (url in urls) {
|
||||||
|
|
||||||
|
status <- tryCatch(
|
||||||
|
renv_bootstrap_download_impl(url, destfile),
|
||||||
|
condition = identity
|
||||||
|
)
|
||||||
|
|
||||||
|
if (identical(status, 0L)) {
|
||||||
|
message("OK")
|
||||||
|
return(destfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message("FAILED")
|
||||||
|
return(FALSE)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_download_github <- function(version) {
|
||||||
|
|
||||||
|
enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE")
|
||||||
|
if (!identical(enabled, "TRUE"))
|
||||||
|
return(FALSE)
|
||||||
|
|
||||||
|
# prepare download options
|
||||||
|
pat <- Sys.getenv("GITHUB_PAT")
|
||||||
|
if (nzchar(Sys.which("curl")) && nzchar(pat)) {
|
||||||
|
fmt <- "--location --fail --header \"Authorization: token %s\""
|
||||||
|
extra <- sprintf(fmt, pat)
|
||||||
|
saved <- options("download.file.method", "download.file.extra")
|
||||||
|
options(download.file.method = "curl", download.file.extra = extra)
|
||||||
|
on.exit(do.call(base::options, saved), add = TRUE)
|
||||||
|
} else if (nzchar(Sys.which("wget")) && nzchar(pat)) {
|
||||||
|
fmt <- "--header=\"Authorization: token %s\""
|
||||||
|
extra <- sprintf(fmt, pat)
|
||||||
|
saved <- options("download.file.method", "download.file.extra")
|
||||||
|
options(download.file.method = "wget", download.file.extra = extra)
|
||||||
|
on.exit(do.call(base::options, saved), add = TRUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
message("* Downloading renv ", version, " from GitHub ... ", appendLF = FALSE)
|
||||||
|
|
||||||
|
url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version)
|
||||||
|
name <- sprintf("renv_%s.tar.gz", version)
|
||||||
|
destfile <- file.path(tempdir(), name)
|
||||||
|
|
||||||
|
status <- tryCatch(
|
||||||
|
renv_bootstrap_download_impl(url, destfile),
|
||||||
|
condition = identity
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!identical(status, 0L)) {
|
||||||
|
message("FAILED")
|
||||||
|
return(FALSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
message("OK")
|
||||||
|
return(destfile)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_install <- function(version, tarball, library) {
|
||||||
|
|
||||||
|
# attempt to install it into project library
|
||||||
|
message("* Installing renv ", version, " ... ", appendLF = FALSE)
|
||||||
|
dir.create(library, showWarnings = FALSE, recursive = TRUE)
|
||||||
|
|
||||||
|
# invoke using system2 so we can capture and report output
|
||||||
|
bin <- R.home("bin")
|
||||||
|
exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R"
|
||||||
|
r <- file.path(bin, exe)
|
||||||
|
args <- c("--vanilla", "CMD", "INSTALL", "-l", shQuote(library), shQuote(tarball))
|
||||||
|
output <- system2(r, args, stdout = TRUE, stderr = TRUE)
|
||||||
|
message("Done!")
|
||||||
|
|
||||||
|
# check for successful install
|
||||||
|
status <- attr(output, "status")
|
||||||
|
if (is.numeric(status) && !identical(status, 0L)) {
|
||||||
|
header <- "Error installing renv:"
|
||||||
|
lines <- paste(rep.int("=", nchar(header)), collapse = "")
|
||||||
|
text <- c(header, lines, output)
|
||||||
|
writeLines(text, con = stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
status
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_prefix <- function() {
|
||||||
|
|
||||||
|
# construct version prefix
|
||||||
|
version <- paste(R.version$major, R.version$minor, sep = ".")
|
||||||
|
prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-")
|
||||||
|
|
||||||
|
# include SVN revision for development versions of R
|
||||||
|
# (to avoid sharing platform-specific artefacts with released versions of R)
|
||||||
|
devel <-
|
||||||
|
identical(R.version[["status"]], "Under development (unstable)") ||
|
||||||
|
identical(R.version[["nickname"]], "Unsuffered Consequences")
|
||||||
|
|
||||||
|
if (devel)
|
||||||
|
prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r")
|
||||||
|
|
||||||
|
# build list of path components
|
||||||
|
components <- c(prefix, R.version$platform)
|
||||||
|
|
||||||
|
# include prefix if provided by user
|
||||||
|
prefix <- Sys.getenv("RENV_PATHS_PREFIX")
|
||||||
|
if (nzchar(prefix))
|
||||||
|
components <- c(prefix, components)
|
||||||
|
|
||||||
|
# build prefix
|
||||||
|
paste(components, collapse = "/")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_library_root_name <- function(project) {
|
||||||
|
|
||||||
|
# use project name as-is if requested
|
||||||
|
asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE")
|
||||||
|
if (asis)
|
||||||
|
return(basename(project))
|
||||||
|
|
||||||
|
# otherwise, disambiguate based on project's path
|
||||||
|
id <- substring(renv_bootstrap_hash_text(project), 1L, 8L)
|
||||||
|
paste(basename(project), id, sep = "-")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_library_root <- function(project) {
|
||||||
|
|
||||||
|
path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA)
|
||||||
|
if (!is.na(path))
|
||||||
|
return(path)
|
||||||
|
|
||||||
|
path <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA)
|
||||||
|
if (!is.na(path)) {
|
||||||
|
name <- renv_bootstrap_library_root_name(project)
|
||||||
|
return(file.path(path, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.path(project, "renv/library")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_validate_version <- function(version) {
|
||||||
|
|
||||||
|
loadedversion <- utils::packageDescription("renv", fields = "Version")
|
||||||
|
if (version == loadedversion)
|
||||||
|
return(TRUE)
|
||||||
|
|
||||||
|
# assume four-component versions are from GitHub; three-component
|
||||||
|
# versions are from CRAN
|
||||||
|
components <- strsplit(loadedversion, "[.-]")[[1]]
|
||||||
|
remote <- if (length(components) == 4L)
|
||||||
|
paste("rstudio/renv", loadedversion, sep = "@")
|
||||||
|
else
|
||||||
|
paste("renv", loadedversion, sep = "@")
|
||||||
|
|
||||||
|
fmt <- paste(
|
||||||
|
"renv %1$s was loaded from project library, but this project is configured to use renv %2$s.",
|
||||||
|
"Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.",
|
||||||
|
"Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.",
|
||||||
|
sep = "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg <- sprintf(fmt, loadedversion, version, remote)
|
||||||
|
warning(msg, call. = FALSE)
|
||||||
|
|
||||||
|
FALSE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_hash_text <- function(text) {
|
||||||
|
|
||||||
|
hashfile <- tempfile("renv-hash-")
|
||||||
|
on.exit(unlink(hashfile), add = TRUE)
|
||||||
|
|
||||||
|
writeLines(text, con = hashfile)
|
||||||
|
tools::md5sum(hashfile)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
renv_bootstrap_load <- function(project, libpath, version) {
|
||||||
|
|
||||||
|
# try to load renv from the project library
|
||||||
|
if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE))
|
||||||
|
return(FALSE)
|
||||||
|
|
||||||
|
# warn if the version of renv loaded does not match
|
||||||
|
renv_bootstrap_validate_version(version)
|
||||||
|
|
||||||
|
# load the project
|
||||||
|
renv::load(project)
|
||||||
|
|
||||||
|
TRUE
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# construct path to library root
|
||||||
|
root <- renv_bootstrap_library_root(project)
|
||||||
|
|
||||||
|
# construct library prefix for platform
|
||||||
|
prefix <- renv_bootstrap_prefix()
|
||||||
|
|
||||||
|
# construct full libpath
|
||||||
|
libpath <- file.path(root, prefix)
|
||||||
|
|
||||||
|
# attempt to load
|
||||||
|
if (renv_bootstrap_load(project, libpath, version))
|
||||||
|
return(TRUE)
|
||||||
|
|
||||||
|
# load failed; inform user we're about to bootstrap
|
||||||
|
prefix <- paste("# Bootstrapping renv", version)
|
||||||
|
postfix <- paste(rep.int("-", 77L - nchar(prefix)), collapse = "")
|
||||||
|
header <- paste(prefix, postfix)
|
||||||
|
message(header)
|
||||||
|
|
||||||
|
# perform bootstrap
|
||||||
|
bootstrap(version, libpath)
|
||||||
|
|
||||||
|
# exit early if we're just testing bootstrap
|
||||||
|
if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA)))
|
||||||
|
return(TRUE)
|
||||||
|
|
||||||
|
# try again to load
|
||||||
|
if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) {
|
||||||
|
message("* Successfully installed and loaded renv ", version, ".")
|
||||||
|
return(renv::load())
|
||||||
|
}
|
||||||
|
|
||||||
|
# failed to download or load renv; warn the user
|
||||||
|
msg <- c(
|
||||||
|
"Failed to find an renv installation: the project will not be loaded.",
|
||||||
|
"Use `renv::activate()` to re-initialize the project."
|
||||||
|
)
|
||||||
|
|
||||||
|
warning(paste(msg, collapse = "\n"), call. = FALSE)
|
||||||
|
|
||||||
|
})
|
||||||
9
pre_commit/resources/empty_template_environment.yml
Normal file
9
pre_commit/resources/empty_template_environment.yml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
channels:
|
||||||
|
- conda-forge
|
||||||
|
- defaults
|
||||||
|
dependencies:
|
||||||
|
# This cannot be empty as otherwise no environment will be created.
|
||||||
|
# We're using openssl here as it is available on all system and will
|
||||||
|
# most likely be always installed anyways.
|
||||||
|
# See https://github.com/conda/conda/issues/9487
|
||||||
|
- openssl
|
||||||
1
pre_commit/resources/empty_template_go.mod
Normal file
1
pre_commit/resources/empty_template_go.mod
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module pre-commit-placeholder-empty-module
|
||||||
4
pre_commit/resources/empty_template_package.json
Normal file
4
pre_commit/resources/empty_template_package.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "pre_commit_placeholder_package",
|
||||||
|
"version": "0.0.0"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package = "pre-commit-package"
|
||||||
|
version = "dev-1"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url = "git+ssh://git@github.com/pre-commit/pre-commit.git"
|
||||||
|
}
|
||||||
|
description = {}
|
||||||
|
dependencies = {}
|
||||||
|
build = {
|
||||||
|
type = "builtin",
|
||||||
|
modules = {},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
Gem::Specification.new do |s|
|
||||||
|
s.name = 'pre_commit_placeholder_package'
|
||||||
|
s.version = '0.0.0'
|
||||||
|
s.summary = 'placeholder gem for pre-commit hooks'
|
||||||
|
s.authors = ['Anthony Sottile']
|
||||||
|
end
|
||||||
4
pre_commit/resources/empty_template_pubspec.yaml
Normal file
4
pre_commit/resources/empty_template_pubspec.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
name: pre_commit_empty_pubspec
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.12.0'
|
||||||
|
executables: {}
|
||||||
20
pre_commit/resources/empty_template_renv.lock
Normal file
20
pre_commit/resources/empty_template_renv.lock
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"R": {
|
||||||
|
"Version": "4.0.3",
|
||||||
|
"Repositories": [
|
||||||
|
{
|
||||||
|
"Name": "CRAN",
|
||||||
|
"URL": "https://cran.rstudio.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Packages": {
|
||||||
|
"renv": {
|
||||||
|
"Package": "renv",
|
||||||
|
"Version": "0.12.5",
|
||||||
|
"Source": "Repository",
|
||||||
|
"Repository": "CRAN",
|
||||||
|
"Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
pre_commit/resources/empty_template_setup.py
Normal file
4
pre_commit/resources/empty_template_setup.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[])
|
||||||
|
|
@ -1,171 +1,20 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env bash
|
||||||
"""File generated by pre-commit: https://pre-commit.com"""
|
# File generated by pre-commit: https://pre-commit.com
|
||||||
from __future__ import print_function
|
# ID: 138fd403232d2ddd5efb44317e38bf03
|
||||||
|
|
||||||
import distutils.spawn
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
Z40 = '0' * 40
|
|
||||||
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
|
||||||
# start templated
|
# start templated
|
||||||
CONFIG = None
|
INSTALL_PYTHON=''
|
||||||
HOOK_TYPE = None
|
ARGS=(hook-impl)
|
||||||
INSTALL_PYTHON = None
|
|
||||||
SKIP_ON_MISSING_CONFIG = None
|
|
||||||
# end templated
|
# end templated
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
ARGS+=(--hook-dir "$HERE" -- "$@")
|
||||||
|
|
||||||
class EarlyExit(RuntimeError):
|
if [ -x "$INSTALL_PYTHON" ]; then
|
||||||
pass
|
exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
|
||||||
|
elif command -v pre-commit > /dev/null; then
|
||||||
|
exec pre-commit "${ARGS[@]}"
|
||||||
class FatalError(RuntimeError):
|
else
|
||||||
pass
|
echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
def _norm_exe(exe):
|
|
||||||
"""Necessary for shebang support on windows.
|
|
||||||
|
|
||||||
roughly lifted from `identify.identify.parse_shebang`
|
|
||||||
"""
|
|
||||||
with open(exe, 'rb') as f:
|
|
||||||
if f.read(2) != b'#!':
|
|
||||||
return ()
|
|
||||||
try:
|
|
||||||
first_line = f.readline().decode('UTF-8')
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
cmd = first_line.split()
|
|
||||||
if cmd[0] == '/usr/bin/env':
|
|
||||||
del cmd[0]
|
|
||||||
return tuple(cmd)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_legacy():
|
|
||||||
if HOOK_TYPE == 'pre-push':
|
|
||||||
stdin = getattr(sys.stdin, 'buffer', sys.stdin).read()
|
|
||||||
else:
|
|
||||||
stdin = None
|
|
||||||
|
|
||||||
legacy_hook = os.path.join(HERE, '{}.legacy'.format(HOOK_TYPE))
|
|
||||||
if os.access(legacy_hook, os.X_OK):
|
|
||||||
cmd = _norm_exe(legacy_hook) + (legacy_hook,) + tuple(sys.argv[1:])
|
|
||||||
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE if stdin else None)
|
|
||||||
proc.communicate(stdin)
|
|
||||||
return proc.returncode, stdin
|
|
||||||
else:
|
|
||||||
return 0, stdin
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_config():
|
|
||||||
cmd = ('git', 'rev-parse', '--show-toplevel')
|
|
||||||
top_level = subprocess.check_output(cmd).decode('UTF-8').strip()
|
|
||||||
cfg = os.path.join(top_level, CONFIG)
|
|
||||||
if os.path.isfile(cfg):
|
|
||||||
pass
|
|
||||||
elif SKIP_ON_MISSING_CONFIG or os.getenv('PRE_COMMIT_ALLOW_NO_CONFIG'):
|
|
||||||
print(
|
|
||||||
'`{}` config file not found. '
|
|
||||||
'Skipping `pre-commit`.'.format(CONFIG),
|
|
||||||
)
|
|
||||||
raise EarlyExit()
|
|
||||||
else:
|
|
||||||
raise FatalError(
|
|
||||||
'No {} file was found\n'
|
|
||||||
'- To temporarily silence this, run '
|
|
||||||
'`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
|
|
||||||
'- To permanently silence this, install pre-commit with the '
|
|
||||||
'--allow-missing-config option\n'
|
|
||||||
'- To uninstall pre-commit run '
|
|
||||||
'`pre-commit uninstall`'.format(CONFIG),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _exe():
|
|
||||||
with open(os.devnull, 'wb') as devnull:
|
|
||||||
for exe in (INSTALL_PYTHON, sys.executable):
|
|
||||||
try:
|
|
||||||
if not subprocess.call(
|
|
||||||
(exe, '-c', 'import pre_commit.main'),
|
|
||||||
stdout=devnull, stderr=devnull,
|
|
||||||
):
|
|
||||||
return (exe, '-m', 'pre_commit.main', 'run')
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if distutils.spawn.find_executable('pre-commit'):
|
|
||||||
return ('pre-commit', 'run')
|
|
||||||
|
|
||||||
raise FatalError(
|
|
||||||
'`pre-commit` not found. Did you forget to activate your virtualenv?',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _rev_exists(rev):
|
|
||||||
return not subprocess.call(('git', 'rev-list', '--quiet', rev))
|
|
||||||
|
|
||||||
|
|
||||||
def _pre_push(stdin):
|
|
||||||
remote = sys.argv[1]
|
|
||||||
|
|
||||||
opts = ()
|
|
||||||
for line in stdin.decode('UTF-8').splitlines():
|
|
||||||
_, local_sha, _, remote_sha = line.split()
|
|
||||||
if local_sha == Z40:
|
|
||||||
continue
|
|
||||||
elif remote_sha != Z40 and _rev_exists(remote_sha):
|
|
||||||
opts = ('--origin', local_sha, '--source', remote_sha)
|
|
||||||
else:
|
|
||||||
# First ancestor not found in remote
|
|
||||||
first_ancestor = subprocess.check_output((
|
|
||||||
'git', 'rev-list', '--max-count=1', '--topo-order',
|
|
||||||
'--reverse', local_sha, '--not', '--remotes={}'.format(remote),
|
|
||||||
)).decode().strip()
|
|
||||||
if not first_ancestor:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
cmd = ('git', 'rev-list', '--max-parents=0', local_sha)
|
|
||||||
roots = set(subprocess.check_output(cmd).decode().splitlines())
|
|
||||||
if first_ancestor in roots:
|
|
||||||
# pushing the whole tree including root commit
|
|
||||||
opts = ('--all-files',)
|
|
||||||
else:
|
|
||||||
cmd = ('git', 'rev-parse', '{}^'.format(first_ancestor))
|
|
||||||
source = subprocess.check_output(cmd).decode().strip()
|
|
||||||
opts = ('--origin', local_sha, '--source', source)
|
|
||||||
|
|
||||||
if opts:
|
|
||||||
return opts
|
|
||||||
else:
|
|
||||||
# An attempt to push an empty changeset
|
|
||||||
raise EarlyExit()
|
|
||||||
|
|
||||||
|
|
||||||
def _opts(stdin):
|
|
||||||
fns = {
|
|
||||||
'commit-msg': lambda _: ('--commit-msg-filename', sys.argv[1]),
|
|
||||||
'pre-commit': lambda _: (),
|
|
||||||
'pre-push': _pre_push,
|
|
||||||
}
|
|
||||||
stage = HOOK_TYPE.replace('pre-', '')
|
|
||||||
return ('--config', CONFIG, '--hook-stage', stage) + fns[HOOK_TYPE](stdin)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
retv, stdin = _run_legacy()
|
|
||||||
try:
|
|
||||||
_validate_config()
|
|
||||||
return retv | subprocess.call(_exe() + _opts(stdin))
|
|
||||||
except EarlyExit:
|
|
||||||
return retv
|
|
||||||
except FatalError as e:
|
|
||||||
print(e.args[0])
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
exit(main())
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue