mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-18 00:24:47 +04:00
Compare commits
1044 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 |
222 changed files with 10742 additions and 3530 deletions
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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,8 +1,6 @@
|
|||
*.egg-info
|
||||
*.py[co]
|
||||
/.coverage
|
||||
/.mypy_cache
|
||||
/.pytest_cache
|
||||
/.tox
|
||||
/dist
|
||||
/venv*
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -1,54 +1,44 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.5.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: double-quote-string-fixer
|
||||
- id: name-tests-test
|
||||
- id: requirements-txt-fixer
|
||||
- id: double-quote-string-fixer
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-typing-imports==1.6.0]
|
||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||
rev: v1.5.2
|
||||
hooks:
|
||||
- id: autopep8
|
||||
- repo: https://github.com/pre-commit/pre-commit
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: validate_manifest
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: [--py3-plus]
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: v2.0.1
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.9.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.16.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
exclude: ^pre_commit/resources/
|
||||
args: [--py310-plus, --add-import, 'from __future__ import annotations']
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: v4.0.0
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
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: v0.770
|
||||
rev: v1.19.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-pyyaml]
|
||||
exclude: ^testing/resources/
|
||||
- repo: meta
|
||||
hooks:
|
||||
- id: check-hooks-apply
|
||||
- id: check-useless-excludes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
- id: validate_manifest
|
||||
name: Validate Pre-Commit Manifest
|
||||
name: validate pre-commit manifest
|
||||
description: This validator validates a pre-commit hooks manifest file
|
||||
entry: pre-commit-validate-manifest
|
||||
entry: pre-commit validate-manifest
|
||||
language: python
|
||||
files: ^(\.pre-commit-hooks\.yaml|hooks\.yaml)$
|
||||
files: ^\.pre-commit-hooks\.yaml$
|
||||
|
|
|
|||
926
CHANGELOG.md
926
CHANGELOG.md
|
|
@ -1,3 +1,925 @@
|
|||
4.5.1 - 2025-12-16
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `language: python` with `repo: local` without `additional_dependencies`.
|
||||
- #3597 PR by @asottile.
|
||||
|
||||
4.5.0 - 2025-11-22
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Add `pre-commit hazmat`.
|
||||
- #3585 PR by @asottile.
|
||||
|
||||
4.4.0 - 2025-11-08
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Add `--fail-fast` option to `pre-commit run`.
|
||||
- #3528 PR by @JulianMaurin.
|
||||
- Upgrade `ruby-build` / `rbenv`.
|
||||
- #3566 PR by @asottile.
|
||||
- #3565 issue by @MRigal.
|
||||
- Add `language: unsupported` / `language: unsupported_script` as aliases
|
||||
for `language: system` / `language: script` (which will eventually be
|
||||
deprecated).
|
||||
- #3577 PR by @asottile.
|
||||
- Add support docker-in-docker detection for cgroups v2.
|
||||
- #3535 PR by @br-rhrbacek.
|
||||
- #3360 issue by @JasonAlt.
|
||||
|
||||
### Fixes
|
||||
- Handle when docker gives `SecurityOptions: null`.
|
||||
- #3537 PR by @asottile.
|
||||
- #3514 issue by @jenstroeger.
|
||||
- Fix error context for invalid `stages` in `.pre-commit-config.yaml`.
|
||||
- #3576 PR by @asottile.
|
||||
|
||||
4.3.0 - 2025-08-09
|
||||
==================
|
||||
|
||||
### Features
|
||||
- `language: docker` / `language: docker_image`: detect rootless docker.
|
||||
- #3446 PR by @matthewhughes934.
|
||||
- #1243 issue by @dkolepp.
|
||||
- `language: julia`: avoid `startup.jl` when executing hooks.
|
||||
- #3496 PR by @ericphanson.
|
||||
- `language: dart`: support latest dart versions which require a higher sdk
|
||||
lower bound.
|
||||
- #3507 PR by @bc-lee.
|
||||
|
||||
4.2.0 - 2025-03-18
|
||||
==================
|
||||
|
||||
### Features
|
||||
- For `language: python` first attempt a versioned python executable for
|
||||
the default language version before consulting a potentially unversioned
|
||||
`sys.executable`.
|
||||
- #3430 PR by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Handle error during conflict detection when a file is named "HEAD"
|
||||
- #3425 PR by @tusharsadhwani.
|
||||
|
||||
4.1.0 - 2025-01-20
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Add `language: julia`.
|
||||
- #3348 PR by @fredrikekre.
|
||||
- #2689 issue @jmuchovej.
|
||||
|
||||
### Fixes
|
||||
- Disable automatic toolchain switching for `language: golang`.
|
||||
- #3304 PR by @AleksaC.
|
||||
- #3300 issue by @AleksaC.
|
||||
- #3149 issue by @nijel.
|
||||
- Fix `language: r` installation when initiated by RStudio.
|
||||
- #3389 PR by @lorenzwalthert.
|
||||
- #3385 issue by @lorenzwalthert.
|
||||
|
||||
|
||||
4.0.1 - 2024-10-08
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `pre-commit migrate-config` for unquoted deprecated stages names with
|
||||
purelib `pyyaml`.
|
||||
- #3324 PR by @asottile.
|
||||
- pre-commit-ci/issues#234 issue by @lorenzwalthert.
|
||||
|
||||
4.0.0 - 2024-10-05
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Improve `pre-commit migrate-config` to handle more yaml formats.
|
||||
- #3301 PR by @asottile.
|
||||
- Handle `stages` deprecation in `pre-commit migrate-config`.
|
||||
- #3302 PR by @asottile.
|
||||
- #2732 issue by @asottile.
|
||||
- Upgrade `ruby-build`.
|
||||
- #3199 PR by @ThisGuyCodes.
|
||||
- Add "sensible regex" warnings to `repo: meta`.
|
||||
- #3311 PR by @asottile.
|
||||
- Add warnings for deprecated `stages` (`commit` -> `pre-commit`, `push` ->
|
||||
`pre-push`, `merge-commit` -> `pre-merge-commit`).
|
||||
- #3312 PR by @asottile.
|
||||
- #3313 PR by @asottile.
|
||||
- #3315 PR by @asottile.
|
||||
- #2732 issue by @asottile.
|
||||
|
||||
### Updating
|
||||
- `language: python_venv` has been removed -- use `language: python` instead.
|
||||
- #3320 PR by @asottile.
|
||||
- #2734 issue by @asottile.
|
||||
|
||||
3.8.0 - 2024-07-28
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Implement health checks for `language: r` so environments are recreated if
|
||||
the system version of R changes.
|
||||
- #3206 issue by @lorenzwalthert.
|
||||
- #3265 PR by @lorenzwalthert.
|
||||
|
||||
3.7.1 - 2024-05-10
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `language: rust` default language version check when `rust-toolchain.toml`
|
||||
is present.
|
||||
- issue by @gaborbernat.
|
||||
- #3201 PR by @asottile.
|
||||
|
||||
3.7.0 - 2024-03-24
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Use a tty for `docker` and `docker_image` hooks when `--color` is specified.
|
||||
- #3122 PR by @glehmann.
|
||||
|
||||
### Fixes
|
||||
- Fix `fail_fast` for individual hooks stopping when previous hooks had failed.
|
||||
- #3167 issue by @tp832944.
|
||||
- #3168 PR by @asottile.
|
||||
|
||||
### Updating
|
||||
- The per-hook behaviour of `fail_fast` was fixed. If you want the pre-3.7.0
|
||||
behaviour, add `fail_fast: true` to all hooks before the last `fail_fast`
|
||||
hook.
|
||||
|
||||
3.6.2 - 2024-02-18
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix building golang hooks during `git commit --all`.
|
||||
- #3130 PR by @asottile.
|
||||
- #2722 issue by @pestanko and @matthewhughes934.
|
||||
|
||||
3.6.1 - 2024-02-10
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Remove `PYTHONEXECUTABLE` from environment when running.
|
||||
- #3110 PR by @untitaker.
|
||||
- Handle staged-files-only with only a crlf diff.
|
||||
- #3126 PR by @asottile.
|
||||
- issue by @tyyrok.
|
||||
|
||||
3.6.0 - 2023-12-09
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Check `minimum_pre_commit_version` first when parsing configs.
|
||||
- #3092 PR by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Fix deprecation warnings for `importlib.resources`.
|
||||
- #3043 PR by @asottile.
|
||||
- Fix deprecation warnings for rmtree.
|
||||
- #3079 PR by @edgarrmondragon.
|
||||
|
||||
### Updating
|
||||
- Drop support for python<3.9.
|
||||
- #3042 PR by @asottile.
|
||||
- #3093 PR by @asottile.
|
||||
|
||||
3.5.0 - 2023-10-13
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Improve performance of `check-hooks-apply` and `check-useless-excludes`.
|
||||
- #2998 PR by @mxr.
|
||||
- #2935 issue by @mxr.
|
||||
|
||||
### Fixes
|
||||
- Use `time.monotonic()` for more accurate hook timing.
|
||||
- #3024 PR by @adamchainz.
|
||||
|
||||
### Updating
|
||||
- Require npm 6.x+ for `language: node` hooks.
|
||||
- #2996 PR by @RoelAdriaans.
|
||||
- #1983 issue by @henryiii.
|
||||
|
||||
3.4.0 - 2023-09-02
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Add `language: haskell`.
|
||||
- #2932 by @alunduil.
|
||||
- Improve cpu count detection when run under cgroups.
|
||||
- #2979 PR by @jdb8.
|
||||
- #2978 issue by @jdb8.
|
||||
|
||||
### Fixes
|
||||
- Handle negative exit codes from hooks receiving posix signals.
|
||||
- #2971 PR by @chriskuehl.
|
||||
- #2970 issue by @chriskuehl.
|
||||
|
||||
3.3.3 - 2023-06-13
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Work around OS packagers setting `--install-dir` / `--bin-dir` in gem settings.
|
||||
- #2905 PR by @jaysoffian.
|
||||
- #2799 issue by @lmilbaum.
|
||||
|
||||
3.3.2 - 2023-05-17
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Work around `r` on windows sometimes double-un-quoting arguments.
|
||||
- #2885 PR by @lorenzwalthert.
|
||||
- #2870 issue by @lorenzwalthert.
|
||||
|
||||
3.3.1 - 2023-05-02
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Work around `git` partial clone bug for `autoupdate` on windows.
|
||||
- #2866 PR by @asottile.
|
||||
- #2865 issue by @adehad.
|
||||
|
||||
3.3.0 - 2023-05-01
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Upgrade ruby-build.
|
||||
- #2846 PR by @jalessio.
|
||||
- Use blobless clone for faster autoupdate.
|
||||
- #2859 PR by @asottile.
|
||||
- Add `-j` / `--jobs` argument to `autoupdate` for parallel execution.
|
||||
- #2863 PR by @asottile.
|
||||
- issue by @gaborbernat.
|
||||
|
||||
3.2.2 - 2023-04-03
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix support for swift >= 5.8.
|
||||
- #2836 PR by @edelabar.
|
||||
- #2835 issue by @kgrobelny-intive.
|
||||
|
||||
3.2.1 - 2023-03-25
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `language_version` for `language: rust` without global `rustup`.
|
||||
- #2823 issue by @daschuer.
|
||||
- #2827 PR by @asottile.
|
||||
|
||||
3.2.0 - 2023-03-17
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Allow `pre-commit`, `pre-push`, and `pre-merge-commit` as `stages`.
|
||||
- #2732 issue by @asottile.
|
||||
- #2808 PR by @asottile.
|
||||
- Add `pre-rebase` hook support.
|
||||
- #2582 issue by @BrutalSimplicity.
|
||||
- #2725 PR by @mgaligniana.
|
||||
|
||||
### Fixes
|
||||
- Remove bulky cargo cache from `language: rust` installs.
|
||||
- #2820 PR by @asottile.
|
||||
|
||||
3.1.1 - 2023-02-27
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `rust` with `language_version` and a non-writable host `RUSTUP_HOME`.
|
||||
- pre-commit-ci/issues#173 by @Swiftb0y.
|
||||
- #2788 by @asottile.
|
||||
|
||||
3.1.0 - 2023-02-22
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix `dotnet` for `.sln`-based hooks for dotnet>=7.0.200.
|
||||
- #2763 PR by @m-rsha.
|
||||
- Prevent stashing when `diff` fails to execute.
|
||||
- #2774 PR by @asottile.
|
||||
- #2773 issue by @strubbly.
|
||||
- Dependencies are no longer sorted in repository key.
|
||||
- #2776 PR by @asottile.
|
||||
|
||||
### Updating
|
||||
- Deprecate `language: python_venv`. Use `language: python` instead.
|
||||
- #2746 PR by @asottile.
|
||||
- #2734 issue by @asottile.
|
||||
|
||||
|
||||
3.0.4 - 2023-02-03
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix hook diff detection for files affected by `--textconv`.
|
||||
- #2743 PR by @adamchainz.
|
||||
- #2743 issue by @adamchainz.
|
||||
|
||||
3.0.3 - 2023-02-01
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Revert "Prevent local `Gemfile` from interfering with hook execution.".
|
||||
- #2739 issue by @Roguelazer.
|
||||
- #2740 PR by @asottile.
|
||||
|
||||
3.0.2 - 2023-01-29
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Prevent local `Gemfile` from interfering with hook execution.
|
||||
- #2727 PR by @asottile.
|
||||
- Fix `language: r`, `repo: local` hooks
|
||||
- pre-commit-ci/issues#107 by @lorenzwalthert.
|
||||
- #2728 PR by @asottile.
|
||||
|
||||
3.0.1 - 2023-01-26
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Ensure coursier hooks are available offline after install.
|
||||
- #2723 PR by @asottile.
|
||||
|
||||
3.0.0 - 2023-01-23
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Make `language: golang` bootstrap `go` if not present.
|
||||
- #2651 PR by @taoufik07.
|
||||
- #2649 issue by @taoufik07.
|
||||
- `language: coursier` now supports `additional_dependencies` and `repo: local`
|
||||
- #2702 PR by @asottile.
|
||||
- Upgrade `ruby-build` to `20221225`.
|
||||
- #2718 PR by @jalessio.
|
||||
|
||||
### Fixes
|
||||
- Improve error message for invalid yaml for `pre-commit autoupdate`.
|
||||
- #2686 PR by @asottile.
|
||||
- #2685 issue by @CarstenGrohmann.
|
||||
- `repo: local` no longer provisions an empty `git` repo.
|
||||
- #2699 PR by @asottile.
|
||||
|
||||
### Updating
|
||||
- Drop support for python<3.8
|
||||
- #2655 PR by @asottile.
|
||||
- Drop support for top-level list, use `pre-commit migrate-config` to update.
|
||||
- #2656 PR by @asottile.
|
||||
- Drop support for `sha` to specify revision, use `pre-commit migrate-config`
|
||||
to update.
|
||||
- #2657 PR by @asottile.
|
||||
- Remove `pre-commit-validate-config` and `pre-commit-validate-manifest`, use
|
||||
`pre-commit validate-config` and `pre-commit validate-manifest` instead.
|
||||
- #2658 PR by @asottile.
|
||||
- `language: golang` hooks must use `go.mod` to specify dependencies
|
||||
- #2672 PR by @taoufik07.
|
||||
|
||||
|
||||
2.21.0 - 2022-12-25
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Require new-enough virtualenv to prevent 3.10 breakage
|
||||
- #2467 PR by @asottile.
|
||||
- Respect aliases with `SKIP` for environment install.
|
||||
- #2480 PR by @kmARC.
|
||||
- #2478 issue by @kmARC.
|
||||
- Allow `pre-commit run --files` against unmerged paths.
|
||||
- #2484 PR by @asottile.
|
||||
- Also apply regex warnings to `repo: local` hooks.
|
||||
- #2524 PR by @chrisRedwine.
|
||||
- #2521 issue by @asottile.
|
||||
- `rust` is now a "first class" language -- supporting `language_version` and
|
||||
installation when not present.
|
||||
- #2534 PR by @Holzhaus.
|
||||
- `r` now uses more-reliable binary installation.
|
||||
- #2460 PR by @lorenzwalthert.
|
||||
- `GIT_ALLOW_PROTOCOL` is now passed through for git operations.
|
||||
- #2555 PR by @asottile.
|
||||
- `GIT_ASKPASS` is now passed through for git operations.
|
||||
- #2564 PR by @mattp-.
|
||||
- Remove `toml` dependency by using `cargo add` directly.
|
||||
- #2568 PR by @m-rsha.
|
||||
- Support `dotnet` hooks which have dotted prefixes.
|
||||
- #2641 PR by @rkm.
|
||||
- #2629 issue by @rkm.
|
||||
|
||||
### Fixes
|
||||
- Properly adjust `--commit-msg-filename` if run from a sub directory.
|
||||
- #2459 PR by @asottile.
|
||||
- Simplify `--intent-to-add` detection by using `git diff`.
|
||||
- #2580 PR by @m-rsha.
|
||||
- Fix `R.exe` selection on windows.
|
||||
- #2605 PR by @lorenzwalthert.
|
||||
- #2599 issue by @SInginc.
|
||||
- Skip default `nuget` source when installing `dotnet` packages.
|
||||
- #2642 PR by @rkm.
|
||||
|
||||
2.20.0 - 2022-07-10
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Expose `source` and `object-name` (positional args) of `prepare-commit-msg`
|
||||
hook as `PRE_COMMIT_COMIT_MSG_SOURCE` and `PRE_COMMIT_COMMIT_OBJECT_NAME`.
|
||||
- #2407 PR by @M-Whitaker.
|
||||
- #2406 issue by @M-Whitaker.
|
||||
|
||||
### Fixes
|
||||
- Fix `language: ruby` installs when `--user-install` is set in gemrc.
|
||||
- #2394 PR by @narpfel.
|
||||
- #2393 issue by @narpfel.
|
||||
- Adjust pty setup for solaris.
|
||||
- #2390 PR by @gaige.
|
||||
- #2389 issue by @gaige.
|
||||
- Remove unused `--config` option from `gc`, `sample-config`,
|
||||
`validate-config`, `validate-manifest` sub-commands.
|
||||
- #2429 PR by @asottile.
|
||||
|
||||
2.19.0 - 2022-05-05
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Allow multiple outputs from `language: dotnet` hooks.
|
||||
- #2332 PR by @WallucePinkham.
|
||||
- Add more information to `healthy()` failure.
|
||||
- #2348 PR by @asottile.
|
||||
- Upgrade ruby-build.
|
||||
- #2342 PR by @jalessio.
|
||||
- Add `pre-commit validate-config` / `pre-commit validate-manifest` and
|
||||
deprecate `pre-commit-validate-config` and `pre-commit-validate-manifest`.
|
||||
- #2362 PR by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Fix `pre-push` when pushed ref contains spaces.
|
||||
- #2345 PR by @wwade.
|
||||
- #2344 issue by @wwade.
|
||||
|
||||
### Updating
|
||||
- Change `pre-commit-validate-config` / `pre-commit-validate-manifest` to
|
||||
`pre-commit validate-config` / `pre-commit validate-manifest`.
|
||||
- #2362 PR by @asottile.
|
||||
|
||||
2.18.1 - 2022-04-02
|
||||
===================
|
||||
|
||||
### Fixes
|
||||
- Fix regression for `repo: local` hooks running `python<3.7`
|
||||
- #2324 PR by @asottile.
|
||||
|
||||
2.18.0 - 2022-04-02
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Keep `GIT_HTTP_PROXY_AUTHMETHOD` in git environ.
|
||||
- #2272 PR by @VincentBerthier.
|
||||
- #2271 issue by @VincentBerthier.
|
||||
- Support both `cs` and `coursier` executables for coursier hooks.
|
||||
- #2293 PR by @Holzhaus.
|
||||
- Include more information in errors for `language_version` /
|
||||
`additional_dependencies` for languages which do not support them.
|
||||
- #2315 PR by @asottile.
|
||||
- Have autoupdate preferentially pick tags which look like versions when
|
||||
there are multiple equivalent tags.
|
||||
- #2312 PR by @mblayman.
|
||||
- #2311 issue by @mblayman.
|
||||
- Upgrade `ruby-build`.
|
||||
- #2319 PR by @jalessio.
|
||||
- Add top level `default_install_hook_types` which will be installed when
|
||||
`--hook-types` is not specified in `pre-commit install`.
|
||||
- #2322 PR by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Fix typo in help message for `--from-ref` and `--to-ref`.
|
||||
- #2266 PR by @leetrout.
|
||||
- Prioritize binary builds for R dependencies.
|
||||
- #2277 PR by @lorenzwalthert.
|
||||
- Fix handling of git worktrees.
|
||||
- #2252 PR by @daschuer.
|
||||
- Fix handling of `$R_HOME` for R hooks.
|
||||
- #2301 PR by @jeff-m-sullivan.
|
||||
- #2300 issue by @jeff-m-sullivan.
|
||||
- Fix a rare race condition in change stashing.
|
||||
- #2323 PR by @asottile.
|
||||
- #2287 issue by @ian-h-chamberlain.
|
||||
|
||||
### Updating
|
||||
- Remove python3.6 support. Note that pre-commit still supports running hooks
|
||||
written in older versions, but pre-commit itself requires python 3.7+.
|
||||
- #2215 PR by @asottile.
|
||||
- pre-commit has migrated from the `master` branch to `main`.
|
||||
- #2302 PR by @asottile.
|
||||
|
||||
2.17.0 - 2022-01-18
|
||||
===================
|
||||
|
||||
### Features
|
||||
- add warnings for regexes containing `[\\/]`.
|
||||
- #2151 issue by @sanjioh.
|
||||
- #2154 PR by @kuviokelluja.
|
||||
- upgrade supported ruby versions.
|
||||
- #2205 PR by @jalessio.
|
||||
- allow `language: conda` to use `mamba` or `micromamba` via
|
||||
`PRE_COMMIT_USE_MAMBA=1` or `PRE_COMMIT_USE_MICROMAMBA=1` respectively.
|
||||
- #2204 issue by @janjagusch.
|
||||
- #2207 PR by @xhochy.
|
||||
- display `git --version` in error report.
|
||||
- #2210 PR by @asottile.
|
||||
- add `language: lua` as a supported language.
|
||||
- #2158 PR by @mblayman.
|
||||
|
||||
### Fixes
|
||||
- temporarily add `setuptools` to the zipapp.
|
||||
- #2122 issue by @andreoliwa.
|
||||
- a737d5f commit by @asottile.
|
||||
- use `go install` instead of `go get` for go 1.18+ support.
|
||||
- #2161 PR by @schmir.
|
||||
- fix `language: r` with a local renv and `RENV_PROJECT` set.
|
||||
- #2170 PR by @lorenzwalthert.
|
||||
- forbid overriding `entry` in `language: meta` hooks which breaks them.
|
||||
- #2180 issue by @DanKaplanSES.
|
||||
- #2181 PR by @asottile.
|
||||
- always use `#!/bin/sh` on windows for hook script.
|
||||
- #2182 issue by @hushigome-visco.
|
||||
- #2187 PR by @asottile.
|
||||
|
||||
2.16.0 - 2021-11-30
|
||||
===================
|
||||
|
||||
### Features
|
||||
- add warning for regexes containing `[\/]` or `[/\\]`.
|
||||
- #2053 PR by @radek-sprta.
|
||||
- #2043 issue by @asottile.
|
||||
- move hook template back to `bash` resolving shebang-portability issues.
|
||||
- #2065 PR by @asottile.
|
||||
- add support for `fail_fast` at the individual hook level.
|
||||
- #2097 PR by @colens3.
|
||||
- #1143 issue by @potiuk.
|
||||
- allow passthrough of `GIT_CONFIG_KEY_*`, `GIT_CONFIG_VALUE_*`, and
|
||||
`GIT_CONFIG_COUNT`.
|
||||
- #2136 PR by @emzeat.
|
||||
|
||||
### Fixes
|
||||
- fix pre-commit autoupdate for `core.useBuiltinFSMonitor=true` on windows.
|
||||
- #2047 PR by @asottile.
|
||||
- #2046 issue by @lcnittl.
|
||||
- fix temporary file stashing with for `submodule.recurse=1`.
|
||||
- #2071 PR by @asottile.
|
||||
- #2063 issue by @a666.
|
||||
- ban broken importlib-resources versions.
|
||||
- #2098 PR by @asottile.
|
||||
- replace `exit(...)` with `raise SystemExit(...)` for portability.
|
||||
- #2103 PR by @asottile.
|
||||
- #2104 PR by @asottile.
|
||||
|
||||
|
||||
2.15.0 - 2021-09-02
|
||||
===================
|
||||
|
||||
### Features
|
||||
- add support for hooks written in `dart`.
|
||||
- #2027 PR by @asottile.
|
||||
- add support for `post-rewrite` hooks.
|
||||
- #2036 PR by @uSpike.
|
||||
- #2035 issue by @uSpike.
|
||||
|
||||
### Fixes
|
||||
- fix `check-useless-excludes` with exclude matching broken symlink.
|
||||
- #2029 PR by @asottile.
|
||||
- #2019 issue by @pkoch.
|
||||
- eliminate duplicate mutable sha warning messages for `pre-commit autoupdate`.
|
||||
- #2030 PR by @asottile.
|
||||
- #2010 issue by @graingert.
|
||||
|
||||
2.14.1 - 2021-08-28
|
||||
===================
|
||||
|
||||
### Fixes
|
||||
- fix force-push of disparate histories using git>=2.28.
|
||||
- #2005 PR by @asottile.
|
||||
- #2002 issue by @bogusfocused.
|
||||
- fix `check-useless-excludes` and `check-hooks-apply` matching non-root
|
||||
`.pre-commit-config.yaml`.
|
||||
- #2026 PR by @asottile.
|
||||
- pre-commit-ci/issues#84 issue by @billsioros.
|
||||
|
||||
2.14.0 - 2021-08-06
|
||||
===================
|
||||
|
||||
### Features
|
||||
- During `pre-push` hooks, expose local branch as `PRE_COMMIT_LOCAL_BRANCH`.
|
||||
- #1947 PR by @FlorentClarret.
|
||||
- #1410 issue by @MaicoTimmerman.
|
||||
- Improve container id detection for docker-beside-docker with custom hostname.
|
||||
- #1919 PR by @adarnimrod.
|
||||
- #1918 issue by @adarnimrod.
|
||||
|
||||
### Fixes
|
||||
- Read legacy hooks in an encoding-agnostic way.
|
||||
- #1943 PR by @asottile.
|
||||
- #1942 issue by @sbienkow-ninja.
|
||||
- Fix execution of docker hooks for docker-in-docker.
|
||||
- #1997 PR by @asottile.
|
||||
- #1978 issue by @robin-moss.
|
||||
|
||||
2.13.0 - 2021-05-21
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Setting `SKIP=...` skips installation as well.
|
||||
- #1875 PR by @asottile.
|
||||
- pre-commit-ci/issues#53 issue by @TylerYep.
|
||||
- Attempt to mount from host with docker-in-docker.
|
||||
- #1888 PR by @okainov.
|
||||
- #1387 issue by @okainov.
|
||||
- Enable `repo: local` for `r` hooks.
|
||||
- #1878 PR by @lorenzwalthert.
|
||||
- Upgrade `ruby-build` and `rbenv`.
|
||||
- #1913 PR by @jalessio.
|
||||
|
||||
### Fixes
|
||||
- Better detect `r` packages.
|
||||
- #1898 PR by @lorenzwalthert.
|
||||
- Avoid warnings with mismatched `renv` versions.
|
||||
- #1841 PR by @lorenzwalthert.
|
||||
- Reproducibly produce ruby tar resources.
|
||||
- #1915 PR by @asottile.
|
||||
|
||||
2.12.1 - 2021-04-16
|
||||
===================
|
||||
|
||||
### Fixes
|
||||
- Fix race condition when stashing files in multiple parallel invocations
|
||||
- #1881 PR by @adamchainz.
|
||||
- #1880 issue by @adamchainz.
|
||||
|
||||
2.12.0 - 2021-04-06
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Upgrade rbenv.
|
||||
- #1854 PR by @asottile.
|
||||
- #1848 issue by @sirosen.
|
||||
|
||||
### Fixes
|
||||
- Give command length a little more room when running batch files on windows
|
||||
so underlying commands can expand further.
|
||||
- #1864 PR by @asottile.
|
||||
- pre-commit/mirrors-prettier#7 issue by @DeltaXWizard.
|
||||
- Fix permissions of root folder in ruby archives.
|
||||
- #1868 PR by @asottile.
|
||||
|
||||
2.11.1 - 2021-03-09
|
||||
===================
|
||||
|
||||
### Fixes
|
||||
- Fix r hooks when hook repo is a package
|
||||
- #1831 PR by @lorenzwalthert.
|
||||
|
||||
2.11.0 - 2021-03-07
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Improve warning for mutable ref.
|
||||
- #1809 PR by @JamMarHer.
|
||||
- Add support for `post-merge` hook.
|
||||
- #1800 PR by @psacawa.
|
||||
- #1762 issue by @psacawa.
|
||||
- Add `r` as a supported hook language.
|
||||
- #1799 PR by @lorenzwalthert.
|
||||
|
||||
### Fixes
|
||||
- Fix `pre-commit install` on `subst` / network drives on windows.
|
||||
- #1814 PR by @asottile.
|
||||
- #1802 issue by @goroderickgo.
|
||||
- Fix installation of `local` golang repositories for go 1.16.
|
||||
- #1818 PR by @rafikdraoui.
|
||||
- #1815 issue by @rafikdraoui.
|
||||
|
||||
2.10.1 - 2021-02-06
|
||||
===================
|
||||
|
||||
### Fixes
|
||||
- Fix `language: golang` repositories containing recursive submodules
|
||||
- #1788 issue by @gaurav517.
|
||||
- #1789 PR by @paulhfischer.
|
||||
|
||||
2.10.0 - 2021-01-27
|
||||
===================
|
||||
|
||||
### Features
|
||||
- Allow `ci` as a top-level map for configuration for https://pre-commit.ci
|
||||
- #1735 PR by @asottile.
|
||||
- Add warning for mutable `rev` in configuration
|
||||
- #1715 PR by @paulhfischer.
|
||||
- #974 issue by @asottile.
|
||||
- Add warning for `/*` in top-level `files` / `exclude` regexes
|
||||
- #1750 PR by @paulhfischer.
|
||||
- #1702 issue by @asottile.
|
||||
- Expose `PRE_COMMIT_REMOTE_BRANCH` environment variable during `pre-push`
|
||||
hooks
|
||||
- #1770 PR by @surafelabebe.
|
||||
- Produce error message for `language` / `language_version` for non-installable
|
||||
languages
|
||||
- #1771 PR by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Fix execution in worktrees in subdirectories of bare repositories
|
||||
- #1778 PR by @asottile.
|
||||
- #1777 issue by @s0undt3ch.
|
||||
|
||||
2.9.3 - 2020-12-07
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix crash on cygwin mismatch check outside of a git directory
|
||||
- #1721 PR by @asottile.
|
||||
- #1720 issue by @chronoB.
|
||||
- Fix cleanup code on docker volumes for go
|
||||
- #1725 PR by @fsouza.
|
||||
- Fix working directory detection on SUBST drives on windows
|
||||
- #1727 PR by @mrogaski.
|
||||
- #1610 issue by @jcameron73.
|
||||
|
||||
2.9.2 - 2020-11-25
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix default value for `types_or` so `symlink` and `directory` can be matched
|
||||
- #1716 PR by @asottile.
|
||||
- #1718 issue by @CodeBleu.
|
||||
|
||||
2.9.1 - 2020-11-25
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Improve error message for "hook goes missing"
|
||||
- #1709 PR by @paulhfischer.
|
||||
- #1708 issue by @theod07.
|
||||
- Add warning for `/*` in `files` / `exclude` regexes
|
||||
- #1707 PR by @paulhfischer.
|
||||
- #1702 issue by @asottile.
|
||||
- Fix `healthy()` check for `language: python` on windows when the base
|
||||
executable has non-ascii characters.
|
||||
- #1713 PR by @asottile.
|
||||
- #1711 issue by @Najiva.
|
||||
|
||||
2.9.0 - 2020-11-21
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Add `types_or` which allows matching multiple disparate `types` in a hook
|
||||
- #1677 by @MarcoGorelli.
|
||||
- #607 by @asottile.
|
||||
- Add Github Sponsors / Open Collective links
|
||||
- https://github.com/sponsors/asottile
|
||||
- https://opencollective.com/pre-commit
|
||||
|
||||
### Fixes
|
||||
- Improve cleanup for `language: dotnet`
|
||||
- #1678 by @rkm.
|
||||
- Fix "xargs" when running windows batch files
|
||||
- #1686 PR by @asottile.
|
||||
- #1604 issue by @apietrzak.
|
||||
- #1604 issue by @ufwtlsb.
|
||||
- Fix conflict with external `rbenv` and `language_version: default`
|
||||
- #1700 PR by @asottile.
|
||||
- #1699 issue by @abuxton.
|
||||
- Improve performance of `git status` / `git diff` commands by ignoring
|
||||
submodules
|
||||
- #1704 PR by @Vynce.
|
||||
- #1701 issue by @Vynce.
|
||||
|
||||
2.8.2 - 2020-10-30
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix installation of ruby hooks with `language_version: default`
|
||||
- #1671 issue by @aerickson.
|
||||
- #1672 PR by @asottile.
|
||||
|
||||
2.8.1 - 2020-10-28
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Allow default `language_version` of `system` when the homedir is `/`
|
||||
- #1669 PR by @asottile.
|
||||
|
||||
2.8.0 - 2020-10-28
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Update `rbenv` / `ruby-build`
|
||||
- #1612 issue by @tdeo.
|
||||
- #1614 PR by @asottile.
|
||||
- Update `sample-config` versions
|
||||
- #1611 PR by @mcsitter.
|
||||
- Add new language: `dotnet`
|
||||
- #1598 by @rkm.
|
||||
- Add `--negate` option to `language: pygrep` hooks
|
||||
- #1643 PR by @MarcoGorelli.
|
||||
- Add zipapp support
|
||||
- #1616 PR by @asottile.
|
||||
- Run pre-commit through https://pre-commit.ci
|
||||
- #1662 PR by @asottile.
|
||||
- Add new language: `coursier` (a jvm-based package manager)
|
||||
- #1633 PR by @JosephMoniz.
|
||||
- Exit with distinct codes: 1 (user error), 3 (unexpected error), 130 (^C)
|
||||
- #1601 PR by @int3l.
|
||||
|
||||
### Fixes
|
||||
- Improve `healthy()` check for `language: node` + `language_version: system`
|
||||
hooks when the system executable goes missing.
|
||||
- pre-commit/action#45 issue by @KOliver94.
|
||||
- #1589 issue by @asottile.
|
||||
- #1590 PR by @asottile.
|
||||
- Fix excess whitespace in error log traceback
|
||||
- #1592 PR by @asottile.
|
||||
- Fix posixlike shebang invocations with shim executables of the git hook
|
||||
script on windows.
|
||||
- #1593 issue by @Celeborn2BeAlive.
|
||||
- #1595 PR by @Celeborn2BeAlive.
|
||||
- Remove hard-coded `C:\PythonXX\python.exe` path on windows as it caused
|
||||
confusion (and `virtualenv` can sometimes do better)
|
||||
- #1599 PR by @asottile.
|
||||
- Fix `language: ruby` hooks when `--format-executable` is present in a gemrc
|
||||
- issue by `Rainbow Tux` (discord).
|
||||
- #1603 PR by @asottile.
|
||||
- Move `cygwin` / `win32` mismatch error earlier to catch msys2 mismatches
|
||||
- #1605 issue by @danyeaw.
|
||||
- #1606 PR by @asottile.
|
||||
- Remove `-p` workaround for old `virtualenv`
|
||||
- #1617 PR by @asottile.
|
||||
- Fix `language: node` installations to not symlink outside of the environment
|
||||
- pre-commit-ci/issues#2 issue by @DanielJSottile.
|
||||
- #1667 PR by @asottile.
|
||||
- Don't identify shim executables as valid `system` for defaulting
|
||||
`language_version` for `language: node` / `language: ruby`
|
||||
- #1658 issue by @adithyabsk.
|
||||
- #1668 PR by @asottile.
|
||||
|
||||
|
||||
2.7.1 - 2020-08-23
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Improve performance of docker hooks by removing slow `ps` call
|
||||
- #1572 PR by @rkm.
|
||||
- #1569 issue by @asottile.
|
||||
- Fix un-`healthy()` invalidation followed by install being reported as
|
||||
un-`healthy()`.
|
||||
- #1576 PR by @asottile.
|
||||
- #1575 issue by @jab.
|
||||
- Fix rare file race condition on windows with `os.replace()`
|
||||
- #1577 PR by @asottile.
|
||||
|
||||
2.7.0 - 2020-08-22
|
||||
==================
|
||||
|
||||
### Features
|
||||
- Produce error message if an environment is immediately unhealthy
|
||||
- #1535 PR by @asottile.
|
||||
- Add --no-allow-missing-config option to init-templatedir
|
||||
- #1539 PR by @singergr.
|
||||
- Add warning for old list-style configuration
|
||||
- #1544 PR by @asottile.
|
||||
- Allow pre-commit to succeed on a readonly store.
|
||||
- #1570 PR by @asottile.
|
||||
- #1536 issue by @asottile.
|
||||
|
||||
### Fixes
|
||||
- Fix error messaging when the store directory is readonly
|
||||
- #1546 PR by @asottile.
|
||||
- #1536 issue by @asottile.
|
||||
- Improve `diff` performance with many hooks
|
||||
- #1566 PR by @jhenkens.
|
||||
- #1564 issue by @jhenkens.
|
||||
|
||||
|
||||
2.6.0 - 2020-07-01
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Fix node hooks when `NPM_CONFIG_USERCONFIG` is set
|
||||
- #1521 PR by @asottile.
|
||||
- #1516 issue by @rkm.
|
||||
|
||||
### Features
|
||||
- Skip `rbenv` / `ruby-download` if system ruby is available
|
||||
- #1509 PR by @asottile.
|
||||
- Partial support for ruby on windows (if system ruby is installed)
|
||||
- #1509 PR by @asottile.
|
||||
- #201 issue by @asottile.
|
||||
|
||||
2.5.1 - 2020-06-09
|
||||
==================
|
||||
|
||||
### Fixes
|
||||
- Prevent infinite recursion of post-checkout on clone
|
||||
- #1497 PR by @asottile.
|
||||
- #1496 issue by @admorgan.
|
||||
|
||||
2.5.0 - 2020-06-08
|
||||
==================
|
||||
|
||||
|
|
@ -1048,7 +1970,7 @@ that have helped us get this far!
|
|||
0.18.1 - 2017-09-04
|
||||
===================
|
||||
- Only mention locking when waiting for a lock.
|
||||
- Fix `IOError` during locking in timeout situtation on windows under python 2.
|
||||
- Fix `IOError` during locking in timeout situation on windows under python 2.
|
||||
|
||||
0.18.0 - 2017-09-02
|
||||
===================
|
||||
|
|
@ -1170,7 +2092,7 @@ that have helped us get this far!
|
|||
|
||||
0.13.1 - 2017-02-16
|
||||
===================
|
||||
- Fix dummy gem for ruby local hooks
|
||||
- Fix placeholder gem for ruby local hooks
|
||||
|
||||
0.13.0 - 2017-02-16
|
||||
===================
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
|
||||
- The complete test suite depends on having at least the following installed
|
||||
(possibly not a complete list)
|
||||
- git (A sufficiently newer version is required to run pre-push tests)
|
||||
- python2 (Required by a test which checks different python versions)
|
||||
- git (Version 2.24.0 or above is required to run pre-merge-commit tests)
|
||||
- python3 (Required by a test which checks different python versions)
|
||||
- tox (or virtualenv)
|
||||
- ruby + gem
|
||||
- docker
|
||||
- conda
|
||||
- cargo (required by tests for rust dependencies)
|
||||
- go (required by tests for go dependencies)
|
||||
- swift
|
||||
|
||||
### Setting up an environment
|
||||
|
||||
|
|
@ -61,14 +64,14 @@ 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: node, ruby)
|
||||
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, go, rust,
|
||||
swift, docker).
|
||||
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)
|
||||
|
||||
"third class" is usually the easiest to implement first and is perfectly
|
||||
"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,
|
||||
|
|
@ -89,7 +92,7 @@ language, for example:
|
|||
|
||||
here are the apis that should be implemented for a language
|
||||
|
||||
Note that these are also documented in [`pre_commit/languages/all.py`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/languages/all.py)
|
||||
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`
|
||||
|
||||
|
|
@ -108,20 +111,21 @@ 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 = helpers.basic_default_version
|
||||
get_default_version = lang_base.basic_default_version
|
||||
```
|
||||
|
||||
`python` is currently the only language which implements this api
|
||||
|
||||
#### `healthy`
|
||||
#### `health_check`
|
||||
|
||||
This is used to check whether the installed environment is considered healthy.
|
||||
This function should return `True` or `False`.
|
||||
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
|
||||
healthy = helpers.basic_healthy
|
||||
health_check = lang_base.basic_health_check
|
||||
```
|
||||
|
||||
`python` is currently the only language which implements this api, for python
|
||||
|
|
@ -133,7 +137,7 @@ 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 = helpers.no_install`
|
||||
- (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`
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master)
|
||||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=21&branchName=master)
|
||||
[](https://github.com/pre-commit/pre-commit)
|
||||
[](https://github.com/pre-commit/pre-commit/actions/workflows/main.yml)
|
||||
[](https://results.pre-commit.ci/latest/github/pre-commit/pre-commit/main)
|
||||
|
||||
## pre-commit
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
trigger:
|
||||
branches:
|
||||
include: [master, test-me-*]
|
||||
tags:
|
||||
include: ['*']
|
||||
|
||||
resources:
|
||||
repositories:
|
||||
- repository: asottile
|
||||
type: github
|
||||
endpoint: github
|
||||
name: asottile/azure-pipeline-templates
|
||||
ref: refs/tags/v2.0.0
|
||||
|
||||
jobs:
|
||||
- template: job--pre-commit.yml@asottile
|
||||
- template: job--python-tox.yml@asottile
|
||||
parameters:
|
||||
toxenvs: [py37]
|
||||
os: windows
|
||||
pre_test:
|
||||
- powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts"
|
||||
displayName: Add conda to PATH
|
||||
- powershell: |
|
||||
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\bin"
|
||||
Write-Host "##vso[task.prependpath]C:\Strawberry\perl\site\bin"
|
||||
Write-Host "##vso[task.prependpath]C:\Strawberry\c\bin"
|
||||
displayName: Add strawberry perl to PATH
|
||||
- template: job--python-tox.yml@asottile
|
||||
parameters:
|
||||
toxenvs: [py37]
|
||||
os: linux
|
||||
name_postfix: _latest_git
|
||||
pre_test:
|
||||
- task: UseRubyVersion@0
|
||||
- template: step--git-install.yml
|
||||
- bash: |
|
||||
testing/get-swift.sh
|
||||
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
|
||||
displayName: install swift
|
||||
- template: job--python-tox.yml@asottile
|
||||
parameters:
|
||||
toxenvs: [pypy3, py36, py37, py38, py39]
|
||||
os: linux
|
||||
pre_test:
|
||||
- task: UseRubyVersion@0
|
||||
- bash: |
|
||||
testing/get-swift.sh
|
||||
echo '##vso[task.prependpath]/tmp/swift/usr/bin'
|
||||
displayName: install swift
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pre_commit.main import 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,26 +1,43 @@
|
|||
import argparse
|
||||
from __future__ import annotations
|
||||
|
||||
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 Dict
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import NamedTuple
|
||||
|
||||
import cfgv
|
||||
from identify.identify import ALL_TAGS
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit.error_handler import FatalError
|
||||
from pre_commit.languages.all import all_languages
|
||||
from pre_commit.util import parse_version
|
||||
from pre_commit.util import yaml_load
|
||||
from pre_commit.all_languages import language_names
|
||||
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: str) -> None:
|
||||
if tag not in ALL_TAGS:
|
||||
|
|
@ -30,6 +47,11 @@ def check_type_tag(tag: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def parse_version(s: str) -> tuple[int, ...]:
|
||||
"""poor man's version comparison"""
|
||||
return tuple(int(p) for p in s.split('.'))
|
||||
|
||||
|
||||
def check_min_version(version: str) -> None:
|
||||
if parse_version(version) > parse_version(C.VERSION):
|
||||
raise cfgv.ValidationError(
|
||||
|
|
@ -39,25 +61,192 @@ def check_min_version(version: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def _make_argparser(filenames_help: str) -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('filenames', nargs='*', help=filenames_help)
|
||||
parser.add_argument('-V', '--version', action='version', version=C.VERSION)
|
||||
return parser
|
||||
_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(
|
||||
'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('name', 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('files', check_string_regex, ''),
|
||||
cfgv.Optional('exclude', check_string_regex, '^$'),
|
||||
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(
|
||||
|
|
@ -65,13 +254,13 @@ MANIFEST_HOOK_DICT = cfgv.Map(
|
|||
),
|
||||
cfgv.Optional('args', cfgv.check_array(cfgv.check_string), []),
|
||||
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('description', cfgv.check_string, ''),
|
||||
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
|
||||
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),
|
||||
)
|
||||
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
|
||||
|
|
@ -81,59 +270,81 @@ class InvalidManifestError(FatalError):
|
|||
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(
|
||||
cfgv.load_from_filename,
|
||||
schema=MANIFEST_SCHEMA,
|
||||
load_strategy=yaml_load,
|
||||
load_strategy=_load_manifest_forward_compat,
|
||||
exc_tp=InvalidManifestError,
|
||||
)
|
||||
|
||||
|
||||
def validate_manifest_main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
parser = _make_argparser('Manifest filenames.')
|
||||
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 = 'local'
|
||||
META = 'meta'
|
||||
|
||||
|
||||
class MigrateShaToRev:
|
||||
key = 'rev'
|
||||
class WarnMutableRev(cfgv.Conditional):
|
||||
def check(self, dct: dict[str, Any]) -> None:
|
||||
super().check(dct)
|
||||
|
||||
@staticmethod
|
||||
def _cond(key: str) -> cfgv.Conditional:
|
||||
return cfgv.Conditional(
|
||||
key, cfgv.check_string,
|
||||
condition_key='repo',
|
||||
condition_value=cfgv.NotIn(LOCAL, META),
|
||||
ensure_absent=True,
|
||||
)
|
||||
if self.key in dct:
|
||||
rev = dct[self.key]
|
||||
|
||||
def check(self, dct: Dict[str, Any]) -> None:
|
||||
if dct.get('repo') in {LOCAL, META}:
|
||||
self._cond('rev').check(dct)
|
||||
self._cond('sha').check(dct)
|
||||
elif 'sha' in dct and 'rev' in dct:
|
||||
raise cfgv.ValidationError('Cannot specify both sha and rev')
|
||||
elif 'sha' in dct:
|
||||
self._cond('sha').check(dct)
|
||||
else:
|
||||
self._cond('rev').check(dct)
|
||||
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.',
|
||||
)
|
||||
|
||||
def apply_default(self, dct: Dict[str, Any]) -> None:
|
||||
if 'sha' in dct:
|
||||
dct['rev'] = dct.pop('sha')
|
||||
|
||||
remove_default = cfgv.Required.remove_default
|
||||
class OptionalSensibleRegexAtHook(cfgv.OptionalNoDefault):
|
||||
def check(self, dct: dict[str, Any]) -> None:
|
||||
super().check(dct)
|
||||
|
||||
if '/*' in dct.get(self.key, ''):
|
||||
logger.warning(
|
||||
f'The {self.key!r} field in hook {dct.get("id")!r} is a '
|
||||
f"regex, not a glob -- matching '/*' probably isn't what you "
|
||||
f'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 slashes in the {self.key!r} '
|
||||
fr'field in hook {dct.get("id")!r} to forward slashes, '
|
||||
fr'so you can use / instead of {fwd_slash_re}',
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -147,7 +358,7 @@ def _entry(modname: str) -> str:
|
|||
def warn_unknown_keys_root(
|
||||
extra: Sequence[str],
|
||||
orig_keys: Sequence[str],
|
||||
dct: Dict[str, str],
|
||||
dct: dict[str, str],
|
||||
) -> None:
|
||||
logger.warning(f'Unexpected key(s) present at root: {", ".join(extra)}')
|
||||
|
||||
|
|
@ -155,7 +366,7 @@ def warn_unknown_keys_root(
|
|||
def warn_unknown_keys_repo(
|
||||
extra: Sequence[str],
|
||||
orig_keys: Sequence[str],
|
||||
dct: Dict[str, str],
|
||||
dct: dict[str, str],
|
||||
) -> None:
|
||||
logger.warning(
|
||||
f'Unexpected key(s) present on {dct["repo"]}: {", ".join(extra)}',
|
||||
|
|
@ -166,14 +377,14 @@ _meta = (
|
|||
(
|
||||
'check-hooks-apply', (
|
||||
('name', 'Check hooks apply to the repository'),
|
||||
('files', C.CONFIG_FILE),
|
||||
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
|
||||
('entry', _entry('check_hooks_apply')),
|
||||
),
|
||||
),
|
||||
(
|
||||
'check-useless-excludes', (
|
||||
('name', 'Check for useless excludes'),
|
||||
('files', C.CONFIG_FILE),
|
||||
('files', f'^{re.escape(C.CONFIG_FILE)}$'),
|
||||
('entry', _entry('check_useless_excludes')),
|
||||
),
|
||||
),
|
||||
|
|
@ -186,12 +397,29 @@ _meta = (
|
|||
),
|
||||
)
|
||||
|
||||
|
||||
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 system
|
||||
cfgv.Optional('language', cfgv.check_one_of({'system'}), 'system'),
|
||||
# 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)
|
||||
|
|
@ -206,6 +434,7 @@ META_HOOK_DICT = cfgv.Map(
|
|||
item
|
||||
for item in MANIFEST_HOOK_DICT.items
|
||||
),
|
||||
*_COMMON_HOOK_WARNINGS,
|
||||
)
|
||||
CONFIG_HOOK_DICT = cfgv.Map(
|
||||
'Hook', 'id',
|
||||
|
|
@ -220,7 +449,18 @@ CONFIG_HOOK_DICT = cfgv.Map(
|
|||
cfgv.OptionalNoDefault(item.key, item.check_fn)
|
||||
for item in MANIFEST_HOOK_DICT.items
|
||||
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(
|
||||
'Repository', 'repo',
|
||||
|
|
@ -232,7 +472,7 @@ CONFIG_REPO_DICT = cfgv.Map(
|
|||
'repo', cfgv.NotIn(LOCAL, META),
|
||||
),
|
||||
cfgv.ConditionalRecurse(
|
||||
'hooks', cfgv.Array(MANIFEST_HOOK_DICT),
|
||||
'hooks', cfgv.Array(LOCAL_HOOK_DICT),
|
||||
'repo', LOCAL,
|
||||
),
|
||||
cfgv.ConditionalRecurse(
|
||||
|
|
@ -240,46 +480,62 @@ CONFIG_REPO_DICT = cfgv.Map(
|
|||
'repo', META,
|
||||
),
|
||||
|
||||
MigrateShaToRev(),
|
||||
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(all_languages),
|
||||
*(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in all_languages),
|
||||
cfgv.NoAdditionalKeys(language_names),
|
||||
*(cfgv.Optional(x, cfgv.check_string, C.DEFAULT) for x in language_names),
|
||||
)
|
||||
CONFIG_SCHEMA = cfgv.Map(
|
||||
'Config', None,
|
||||
|
||||
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
|
||||
cfgv.OptionalRecurse(
|
||||
'default_language_version', DEFAULT_LANGUAGE_VERSION, {},
|
||||
),
|
||||
cfgv.Optional(
|
||||
'default_stages',
|
||||
cfgv.check_array(cfgv.check_one_of(C.STAGES)),
|
||||
C.STAGES,
|
||||
),
|
||||
cfgv.Optional('files', check_string_regex, ''),
|
||||
cfgv.Optional('exclude', check_string_regex, '^$'),
|
||||
cfgv.Optional('fail_fast', cfgv.check_bool, False),
|
||||
# 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.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.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)),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -287,31 +543,9 @@ class InvalidConfigError(FatalError):
|
|||
pass
|
||||
|
||||
|
||||
def ordered_load_normalize_legacy_config(contents: str) -> Dict[str, Any]:
|
||||
data = yaml_load(contents)
|
||||
if isinstance(data, list):
|
||||
# TODO: Once happy, issue a deprecation warning and instructions
|
||||
return {'repos': data}
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
load_config = functools.partial(
|
||||
cfgv.load_from_filename,
|
||||
schema=CONFIG_SCHEMA,
|
||||
load_strategy=ordered_load_normalize_legacy_config,
|
||||
load_strategy=yaml_load,
|
||||
exc_tp=InvalidConfigError,
|
||||
)
|
||||
|
||||
|
||||
def validate_config_main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
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,3 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
@ -95,3 +98,12 @@ def use_color(setting: str) -> bool:
|
|||
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,62 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import os.path
|
||||
import re
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import git
|
||||
from pre_commit import output
|
||||
from pre_commit import xargs
|
||||
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.commands.migrate_config import migrate_config
|
||||
from pre_commit.store import Store
|
||||
from pre_commit.util import CalledProcessError
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import tmpdir
|
||||
from pre_commit.util import yaml_dump
|
||||
from pre_commit.util import yaml_load
|
||||
from pre_commit.yaml import yaml_dump
|
||||
from pre_commit.yaml import yaml_load
|
||||
|
||||
|
||||
class RevInfo(NamedTuple):
|
||||
repo: str
|
||||
rev: str
|
||||
frozen: Optional[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'], None)
|
||||
def from_config(cls, config: dict[str, Any]) -> RevInfo:
|
||||
return cls(config['repo'], config['rev'])
|
||||
|
||||
def update(self, tags_only: bool, freeze: bool) -> 'RevInfo':
|
||||
if tags_only:
|
||||
tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--abbrev=0')
|
||||
else:
|
||||
tag_cmd = ('git', 'describe', 'FETCH_HEAD', '--tags', '--exact')
|
||||
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)
|
||||
|
||||
with tmpdir() as tmp:
|
||||
git.init_repo(tmp, self.repo)
|
||||
cmd_output_b('git', 'fetch', 'origin', 'HEAD', '--tags', cwd=tmp)
|
||||
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, cwd=tmp)[1].strip()
|
||||
rev = cmd_output(*tag_cmd)[1].strip()
|
||||
except CalledProcessError:
|
||||
cmd = ('git', 'rev-parse', 'FETCH_HEAD')
|
||||
rev = cmd_output(*cmd, cwd=tmp)[1].strip()
|
||||
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, cwd=tmp)[1].strip()
|
||||
exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
|
||||
if exact != rev:
|
||||
rev, frozen = exact, rev
|
||||
return self._replace(rev=rev, frozen=frozen)
|
||||
|
||||
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):
|
||||
|
|
@ -64,36 +87,40 @@ class RepositoryCannotBeUpdatedError(RuntimeError):
|
|||
|
||||
|
||||
def _check_hooks_still_exist_at_rev(
|
||||
repo_config: Dict[str, Any],
|
||||
repo_config: dict[str, Any],
|
||||
info: RevInfo,
|
||||
store: Store,
|
||||
) -> None:
|
||||
try:
|
||||
path = store.clone(repo_config['repo'], info.rev)
|
||||
manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
|
||||
except InvalidManifestError as e:
|
||||
raise RepositoryCannotBeUpdatedError(str(e))
|
||||
|
||||
# See if any of our hooks were deleted with the new commits
|
||||
hooks = {hook['id'] for hook in repo_config['hooks']}
|
||||
hooks_missing = hooks - {hook['id'] for hook in manifest}
|
||||
hooks_missing = hooks - info.hook_ids
|
||||
if hooks_missing:
|
||||
raise RepositoryCannotBeUpdatedError(
|
||||
f'Cannot update because the tip of HEAD is missing these hooks:\n'
|
||||
f'{", ".join(sorted(hooks_missing))}',
|
||||
f'[{info.repo}] Cannot update because the update target is '
|
||||
f'missing these hooks: {", ".join(sorted(hooks_missing))}',
|
||||
)
|
||||
|
||||
|
||||
REV_LINE_RE = re.compile(
|
||||
r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$', re.DOTALL,
|
||||
)
|
||||
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#]+)(.*)(\r?\n)$')
|
||||
|
||||
|
||||
def _original_lines(
|
||||
path: str,
|
||||
rev_infos: List[Optional[RevInfo]],
|
||||
rev_infos: list[RevInfo | None],
|
||||
retry: bool = False,
|
||||
) -> Tuple[List[str], List[int]]:
|
||||
) -> tuple[list[str], list[int]]:
|
||||
"""detect `rev:` lines or reformat the file"""
|
||||
with open(path, newline='') as f:
|
||||
original = f.read()
|
||||
|
|
@ -110,7 +137,7 @@ def _original_lines(
|
|||
return _original_lines(path, rev_infos, retry=True)
|
||||
|
||||
|
||||
def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None:
|
||||
def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
|
||||
lines, idxs = _original_lines(path, rev_infos)
|
||||
|
||||
for idx, rev_info in zip(idxs, rev_infos):
|
||||
|
|
@ -134,49 +161,53 @@ def _write_new_config(path: str, rev_infos: List[Optional[RevInfo]]) -> None:
|
|||
|
||||
def autoupdate(
|
||||
config_file: str,
|
||||
store: Store,
|
||||
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)
|
||||
retv = 0
|
||||
rev_infos: List[Optional[RevInfo]] = []
|
||||
changed = False
|
||||
retv = 0
|
||||
|
||||
config = load_config(config_file)
|
||||
for repo_config in config['repos']:
|
||||
if repo_config['repo'] in {LOCAL, META}:
|
||||
continue
|
||||
config_repos = [
|
||||
repo for repo in load_config(config_file)['repos']
|
||||
if repo['repo'] not in {LOCAL, META}
|
||||
]
|
||||
|
||||
info = RevInfo.from_config(repo_config)
|
||||
if repos and info.repo not in repos:
|
||||
rev_infos.append(None)
|
||||
continue
|
||||
|
||||
output.write(f'Updating {info.repo} ... ')
|
||||
new_info = info.update(tags_only=tags_only, freeze=freeze)
|
||||
try:
|
||||
_check_hooks_still_exist_at_rev(repo_config, new_info, store)
|
||||
except RepositoryCannotBeUpdatedError as error:
|
||||
output.write_line(error.args[0])
|
||||
rev_infos.append(None)
|
||||
retv = 1
|
||||
continue
|
||||
|
||||
if new_info.rev != info.rev:
|
||||
changed = True
|
||||
if new_info.frozen:
|
||||
updated_to = f'{new_info.frozen} (frozen)'
|
||||
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:
|
||||
updated_to = new_info.rev
|
||||
msg = f'updating {info.rev} -> {updated_to}.'
|
||||
output.write_line(msg)
|
||||
rev_infos.append(new_info)
|
||||
else:
|
||||
output.write_line('already up to date.')
|
||||
rev_infos.append(None)
|
||||
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:
|
||||
_write_new_config(config_file, rev_infos)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
|
||||
from pre_commit import output
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import output
|
||||
|
|
@ -13,13 +12,14 @@ 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],
|
||||
all_repos: dict[tuple[str, str], str],
|
||||
unused_repos: set[tuple[str, str]],
|
||||
repo: dict[str, Any],
|
||||
) -> None:
|
||||
if repo['repo'] == META:
|
||||
return
|
||||
|
|
@ -27,7 +27,8 @@ def _mark_used_repos(
|
|||
for hook in repo['hooks']:
|
||||
deps = hook.get('additional_dependencies')
|
||||
unused_repos.discard((
|
||||
store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
|
||||
store.db_repo_name(repo['repo'], deps),
|
||||
C.LOCAL_REPO_VERSION,
|
||||
))
|
||||
else:
|
||||
key = (repo['repo'], repo['rev'])
|
||||
|
|
@ -57,34 +58,41 @@ def _mark_used_repos(
|
|||
))
|
||||
|
||||
|
||||
def _gc_repos(store: Store) -> int:
|
||||
configs = store.select_all_configs()
|
||||
repos = store.select_all_repos()
|
||||
def _gc(store: Store) -> int:
|
||||
with store.exclusive_lock(), store.connect() as db:
|
||||
store._create_configs_table(db)
|
||||
|
||||
# delete config paths which do not exist
|
||||
dead_configs = [p for p in configs if not os.path.exists(p)]
|
||||
live_configs = [p for p in configs if os.path.exists(p)]
|
||||
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)
|
||||
|
||||
all_repos = {(repo, ref): path for repo, ref, path in repos}
|
||||
unused_repos = set(all_repos)
|
||||
for config_path in live_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)
|
||||
configs_rows = db.execute('SELECT path FROM configs').fetchall()
|
||||
configs = [path for path, in configs_rows]
|
||||
|
||||
store.delete_configs(dead_configs)
|
||||
for db_repo_name, ref in unused_repos:
|
||||
store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
|
||||
return len(unused_repos)
|
||||
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:
|
||||
with store.exclusive_lock():
|
||||
repos_removed = _gc_repos(store)
|
||||
output.write_line(f'{repos_removed} repo(s) removed.')
|
||||
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())
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os.path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pre_commit.commands.run import run
|
||||
from pre_commit.envcontext import envcontext
|
||||
|
|
@ -18,7 +18,7 @@ def _run_legacy(
|
|||
hook_type: str,
|
||||
hook_dir: str,
|
||||
args: Sequence[str],
|
||||
) -> Tuple[int, bytes]:
|
||||
) -> 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"
|
||||
|
|
@ -69,27 +69,44 @@ def _ns(
|
|||
color: bool,
|
||||
*,
|
||||
all_files: bool = False,
|
||||
from_ref: Optional[str] = None,
|
||||
to_ref: Optional[str] = None,
|
||||
remote_name: Optional[str] = None,
|
||||
remote_url: Optional[str] = None,
|
||||
commit_msg_filename: Optional[str] = None,
|
||||
checkout_type: Optional[str] = None,
|
||||
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.replace('pre-', ''),
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -101,18 +118,21 @@ def _pre_push_ns(
|
|||
color: bool,
|
||||
args: Sequence[str],
|
||||
stdin: bytes,
|
||||
) -> Optional[argparse.Namespace]:
|
||||
) -> argparse.Namespace | None:
|
||||
remote_name = args[0]
|
||||
remote_url = args[1]
|
||||
|
||||
for line in stdin.decode().splitlines():
|
||||
_, local_sha, _, remote_sha = line.split()
|
||||
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:
|
||||
|
|
@ -133,6 +153,8 @@ def _pre_push_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}^')
|
||||
|
|
@ -141,6 +163,8 @@ def _pre_push_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
|
||||
|
|
@ -153,6 +177,8 @@ _EXPECTED_ARG_LENGTH_BY_HOOK = {
|
|||
'post-commit': 0,
|
||||
'pre-commit': 0,
|
||||
'pre-merge-commit': 0,
|
||||
'post-merge': 1,
|
||||
'post-rewrite': 1,
|
||||
'pre-push': 2,
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +190,12 @@ def _check_args_length(hook_type: str, args: Sequence[str]) -> None:
|
|||
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:
|
||||
|
|
@ -181,12 +213,24 @@ def _run_ns(
|
|||
color: bool,
|
||||
args: Sequence[str],
|
||||
stdin: bytes,
|
||||
) -> Optional[argparse.Namespace]:
|
||||
) -> 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', 'prepare-commit-msg'}:
|
||||
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':
|
||||
|
|
@ -194,6 +238,17 @@ def _run_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}')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
from typing import Sequence
|
||||
|
||||
from pre_commit.commands.install_uninstall import install
|
||||
from pre_commit.store import Store
|
||||
|
|
@ -14,11 +15,16 @@ def init_templatedir(
|
|||
config_file: str,
|
||||
store: Store,
|
||||
directory: str,
|
||||
hook_types: Sequence[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=True, git_dir=directory,
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import itertools
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pre_commit import git
|
||||
from pre_commit import output
|
||||
from pre_commit.clientlib import InvalidConfigError
|
||||
from pre_commit.clientlib import load_config
|
||||
from pre_commit.repository import all_hooks
|
||||
from pre_commit.repository import install_hook_envs
|
||||
|
|
@ -21,26 +21,34 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# This is used to identify the hook file we install
|
||||
PRIOR_HASHES = (
|
||||
'4d9958c90bc262f47553e2c073f14cfe',
|
||||
'd8ee923c46731b42cd95cc869add4062',
|
||||
'49fd668cb42069aa1b6048464be5d395',
|
||||
'79f09a650522a87b0da915d0d983b2de',
|
||||
'e358c9dae00eac5d06b38dfdb1e33a8c',
|
||||
b'4d9958c90bc262f47553e2c073f14cfe',
|
||||
b'd8ee923c46731b42cd95cc869add4062',
|
||||
b'49fd668cb42069aa1b6048464be5d395',
|
||||
b'79f09a650522a87b0da915d0d983b2de',
|
||||
b'e358c9dae00eac5d06b38dfdb1e33a8c',
|
||||
)
|
||||
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
|
||||
CURRENT_HASH = b'138fd403232d2ddd5efb44317e38bf03'
|
||||
TEMPLATE_START = '# start templated\n'
|
||||
TEMPLATE_END = '# end templated\n'
|
||||
# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH`
|
||||
# #1312 os.defpath is too restrictive on BSD
|
||||
POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin')
|
||||
SYS_EXE = os.path.basename(os.path.realpath(sys.executable))
|
||||
|
||||
|
||||
def _hook_types(cfg_filename: str, hook_types: list[str] | None) -> list[str]:
|
||||
if hook_types is not None:
|
||||
return hook_types
|
||||
else:
|
||||
try:
|
||||
cfg = load_config(cfg_filename)
|
||||
except InvalidConfigError:
|
||||
return ['pre-commit']
|
||||
else:
|
||||
return cfg['default_install_hook_types']
|
||||
|
||||
|
||||
def _hook_paths(
|
||||
hook_type: str,
|
||||
git_dir: Optional[str] = None,
|
||||
) -> Tuple[str, str]:
|
||||
git_dir = git_dir if git_dir is not None else git.get_git_dir()
|
||||
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'
|
||||
|
||||
|
|
@ -48,37 +56,17 @@ def _hook_paths(
|
|||
def is_our_script(filename: str) -> bool:
|
||||
if not os.path.exists(filename): # pragma: win32 no cover (symlink)
|
||||
return False
|
||||
with open(filename) as f:
|
||||
with open(filename, 'rb') as f:
|
||||
contents = f.read()
|
||||
return any(h in contents for h in (CURRENT_HASH,) + PRIOR_HASHES)
|
||||
|
||||
|
||||
def shebang() -> str:
|
||||
if sys.platform == 'win32':
|
||||
py = SYS_EXE
|
||||
else:
|
||||
exe_choices = [
|
||||
f'python{sys.version_info[0]}.{sys.version_info[1]}',
|
||||
f'python{sys.version_info[0]}',
|
||||
]
|
||||
# avoid searching for bare `python` as it's likely to be python 2
|
||||
if SYS_EXE != 'python':
|
||||
exe_choices.append(SYS_EXE)
|
||||
for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices):
|
||||
if os.access(os.path.join(path, exe), os.X_OK):
|
||||
py = exe
|
||||
break
|
||||
else:
|
||||
py = SYS_EXE
|
||||
return f'#!/usr/bin/env {py}'
|
||||
|
||||
|
||||
def _install_hook_script(
|
||||
config_file: str,
|
||||
hook_type: str,
|
||||
overwrite: bool = False,
|
||||
skip_on_missing_config: bool = False,
|
||||
git_dir: Optional[str] = None,
|
||||
git_dir: str | None = None,
|
||||
) -> None:
|
||||
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
|
||||
|
||||
|
|
@ -100,19 +88,23 @@ def _install_hook_script(
|
|||
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
|
||||
if skip_on_missing_config:
|
||||
args.append('--skip-on-missing-config')
|
||||
params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args}
|
||||
|
||||
with open(hook_path, 'w') as hook_file:
|
||||
contents = resource_text('hook-tmpl')
|
||||
before, rest = contents.split(TEMPLATE_START)
|
||||
to_template, after = rest.split(TEMPLATE_END)
|
||||
_, after = rest.split(TEMPLATE_END)
|
||||
|
||||
before = before.replace('#!/usr/bin/env python3', shebang())
|
||||
# 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)
|
||||
for line in to_template.splitlines():
|
||||
var = line.split()[0]
|
||||
hook_file.write(f'{var} = {params[var]!r}\n')
|
||||
hook_file.write(f'INSTALL_PYTHON={shlex.quote(sys.executable)}\n')
|
||||
args_s = shlex.join(args)
|
||||
hook_file.write(f'ARGS=({args_s})\n')
|
||||
hook_file.write(TEMPLATE_END + after)
|
||||
make_executable(hook_path)
|
||||
|
||||
|
|
@ -122,11 +114,11 @@ def _install_hook_script(
|
|||
def install(
|
||||
config_file: str,
|
||||
store: Store,
|
||||
hook_types: Sequence[str],
|
||||
hook_types: list[str] | None,
|
||||
overwrite: bool = False,
|
||||
hooks: bool = False,
|
||||
skip_on_missing_config: bool = False,
|
||||
git_dir: Optional[str] = None,
|
||||
git_dir: str | None = None,
|
||||
) -> int:
|
||||
if git_dir is None and git.has_core_hookpaths_set():
|
||||
logger.error(
|
||||
|
|
@ -135,7 +127,7 @@ def install(
|
|||
)
|
||||
return 1
|
||||
|
||||
for hook_type in hook_types:
|
||||
for hook_type in _hook_types(config_file, hook_types):
|
||||
_install_hook_script(
|
||||
config_file, hook_type,
|
||||
overwrite=overwrite,
|
||||
|
|
@ -165,11 +157,11 @@ def _uninstall_hook_script(hook_type: str) -> None:
|
|||
output.write_line(f'{hook_type} uninstalled')
|
||||
|
||||
if os.path.exists(legacy_path):
|
||||
os.rename(legacy_path, hook_path)
|
||||
os.replace(legacy_path, hook_path)
|
||||
output.write_line(f'Restored previous hooks to {hook_path}')
|
||||
|
||||
|
||||
def uninstall(hook_types: Sequence[str]) -> int:
|
||||
for hook_type in hook_types:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import re
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import textwrap
|
||||
from collections.abc import Callable
|
||||
|
||||
import cfgv
|
||||
import yaml
|
||||
from yaml.nodes import ScalarNode
|
||||
|
||||
from pre_commit.clientlib import load_config
|
||||
from pre_commit.util import yaml_load
|
||||
|
||||
|
||||
def _indent(s: str) -> str:
|
||||
lines = s.splitlines(True)
|
||||
return ''.join(' ' * 4 + line if line.strip() else line for line in lines)
|
||||
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 _is_header_line(line: str) -> bool:
|
||||
|
|
@ -16,17 +23,17 @@ def _is_header_line(line: str) -> bool:
|
|||
|
||||
|
||||
def _migrate_map(contents: str) -> str:
|
||||
# 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:])
|
||||
|
||||
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:])
|
||||
|
||||
# If they are using the "default" flow style of yaml, this operation
|
||||
# will yield a valid configuration
|
||||
try:
|
||||
|
|
@ -34,24 +41,89 @@ def _migrate_map(contents: str) -> str:
|
|||
yaml_load(trial_contents)
|
||||
contents = trial_contents
|
||||
except yaml.YAMLError:
|
||||
contents = f'{header}repos:\n{_indent(rest)}'
|
||||
contents = f'{header}repos:\n{textwrap.indent(rest, " " * 4)}'
|
||||
|
||||
return contents
|
||||
|
||||
|
||||
def _migrate_sha_to_rev(contents: str) -> str:
|
||||
return re.sub(r'(\n\s+)sha:', r'\1rev:', contents)
|
||||
def _preserve_style(n: ScalarNode, *, s: str) -> str:
|
||||
style = n.style or ''
|
||||
return f'{style}{s}{style}'
|
||||
|
||||
|
||||
def _fix_stage(n: ScalarNode) -> str:
|
||||
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:
|
||||
# ensure that the configuration is a valid pre-commit configuration
|
||||
load_config(config_file)
|
||||
|
||||
with open(config_file) as f:
|
||||
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_sha_to_rev(contents)
|
||||
contents = _migrate_composed(contents)
|
||||
|
||||
if contents != orig_contents:
|
||||
with open(config_file, 'w') as f:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import functools
|
||||
|
|
@ -7,28 +9,25 @@ import re
|
|||
import subprocess
|
||||
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 typing import Collection
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
from identify.identify import tags_from_path
|
||||
|
||||
from pre_commit import color
|
||||
from pre_commit import git
|
||||
from pre_commit import output
|
||||
from pre_commit.all_languages import languages
|
||||
from pre_commit.clientlib import load_config
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages.all import languages
|
||||
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.store import Store
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import EnvironT
|
||||
|
||||
|
||||
logger = logging.getLogger('pre_commit')
|
||||
|
|
@ -59,64 +58,75 @@ def _full_msg(
|
|||
|
||||
|
||||
def filter_by_include_exclude(
|
||||
names: Collection[str],
|
||||
names: Iterable[str],
|
||||
include: str,
|
||||
exclude: str,
|
||||
) -> List[str]:
|
||||
) -> Generator[str]:
|
||||
include_re, exclude_re = re.compile(include), re.compile(exclude)
|
||||
return [
|
||||
return (
|
||||
filename for filename in names
|
||||
if include_re.search(filename)
|
||||
if not exclude_re.search(filename)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class Classifier:
|
||||
def __init__(self, filenames: Collection[str]) -> None:
|
||||
def __init__(self, filenames: Iterable[str]) -> None:
|
||||
self.filenames = [f for f in filenames if os.path.lexists(f)]
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _types_for_file(self, filename: str) -> Set[str]:
|
||||
@functools.cache
|
||||
def _types_for_file(self, filename: str) -> set[str]:
|
||||
return tags_from_path(filename)
|
||||
|
||||
def by_types(
|
||||
self,
|
||||
names: Sequence[str],
|
||||
types: Collection[str],
|
||||
exclude_types: Collection[str],
|
||||
) -> List[str]:
|
||||
types, exclude_types = frozenset(types), frozenset(exclude_types)
|
||||
ret = []
|
||||
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 tags & exclude_types:
|
||||
ret.append(filename)
|
||||
return ret
|
||||
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) -> Tuple[str, ...]:
|
||||
names = self.filenames
|
||||
names = filter_by_include_exclude(names, hook.files, hook.exclude)
|
||||
names = self.by_types(names, hook.types, hook.exclude_types)
|
||||
return tuple(names)
|
||||
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: Collection[str],
|
||||
filenames: Iterable[str],
|
||||
include: str,
|
||||
exclude: str,
|
||||
) -> 'Classifier':
|
||||
) -> 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 = (f.replace(os.sep, os.altsep) for f in filenames)
|
||||
filenames = filter_by_include_exclude(filenames, include, exclude)
|
||||
return Classifier(filenames)
|
||||
|
||||
|
||||
def _get_skips(environ: EnvironT) -> Set[str]:
|
||||
def _get_skips(environ: MutableMapping[str, str]) -> set[str]:
|
||||
skips = environ.get('SKIP', '')
|
||||
return {skip.strip() for skip in skips.split(',') if skip.strip()}
|
||||
|
||||
|
|
@ -132,12 +142,13 @@ def _subtle_line(s: str, use_color: bool) -> None:
|
|||
def _run_single_hook(
|
||||
classifier: Classifier,
|
||||
hook: Hook,
|
||||
skips: Set[str],
|
||||
skips: set[str],
|
||||
cols: int,
|
||||
diff_before: bytes,
|
||||
verbose: bool,
|
||||
use_color: bool,
|
||||
) -> bool:
|
||||
filenames = classifier.filenames_for_hook(hook)
|
||||
) -> tuple[bool, bytes]:
|
||||
filenames = tuple(classifier.filenames_for_hook(hook))
|
||||
|
||||
if hook.id in skips or hook.alias in skips:
|
||||
output.write(
|
||||
|
|
@ -151,6 +162,7 @@ def _run_single_hook(
|
|||
)
|
||||
duration = None
|
||||
retcode = 0
|
||||
diff_after = diff_before
|
||||
files_modified = False
|
||||
out = b''
|
||||
elif not filenames and not hook.always_run:
|
||||
|
|
@ -166,21 +178,29 @@ def _run_single_hook(
|
|||
)
|
||||
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))
|
||||
|
||||
diff_cmd = ('git', 'diff', '--no-ext-diff')
|
||||
diff_before = cmd_output_b(*diff_cmd, retcode=None)
|
||||
if not hook.pass_filenames:
|
||||
filenames = ()
|
||||
time_before = time.time()
|
||||
time_before = time.monotonic()
|
||||
language = languages[hook.language]
|
||||
retcode, out = language.run_hook(hook, filenames, use_color)
|
||||
duration = round(time.time() - time_before, 2) or 0
|
||||
diff_after = cmd_output_b(*diff_cmd, retcode=None)
|
||||
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 the hook makes changes, fail the commit
|
||||
files_modified = diff_before != diff_after
|
||||
|
|
@ -212,7 +232,7 @@ def _run_single_hook(
|
|||
output.write_line_b(out.strip(), logfile_name=hook.log_file)
|
||||
output.write_line()
|
||||
|
||||
return files_modified or bool(retcode)
|
||||
return files_modified or bool(retcode), diff_after
|
||||
|
||||
|
||||
def _compute_cols(hooks: Sequence[Hook]) -> int:
|
||||
|
|
@ -230,9 +250,12 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
|
|||
return max(cols, 80)
|
||||
|
||||
|
||||
def _all_filenames(args: argparse.Namespace) -> Collection[str]:
|
||||
def _all_filenames(args: argparse.Namespace) -> Iterable[str]:
|
||||
# these hooks do not operate on files
|
||||
if args.hook_stage in {'post-checkout', 'post-commit'}:
|
||||
if args.hook_stage in {
|
||||
'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,)
|
||||
|
|
@ -248,27 +271,37 @@ def _all_filenames(args: argparse.Namespace) -> Collection[str]:
|
|||
return git.get_staged_files()
|
||||
|
||||
|
||||
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],
|
||||
config: dict[str, Any],
|
||||
hooks: Sequence[Hook],
|
||||
skips: set[str],
|
||||
args: argparse.Namespace,
|
||||
environ: EnvironT,
|
||||
) -> int:
|
||||
"""Actually run the hooks."""
|
||||
skips = _get_skips(environ)
|
||||
cols = _compute_cols(hooks)
|
||||
classifier = Classifier.from_config(
|
||||
_all_filenames(args), config['files'], config['exclude'],
|
||||
)
|
||||
retval = 0
|
||||
prior_diff = _get_diff()
|
||||
for hook in hooks:
|
||||
retval |= _run_single_hook(
|
||||
classifier, hook, skips, cols,
|
||||
current_retval, prior_diff = _run_single_hook(
|
||||
classifier, hook, skips, cols, prior_diff,
|
||||
verbose=args.verbose, use_color=args.color,
|
||||
)
|
||||
if retval and config['fail_fast']:
|
||||
retval |= current_retval
|
||||
fail_fast = (config['fail_fast'] or hook.fail_fast or args.fail_fast)
|
||||
if current_retval and fail_fast:
|
||||
break
|
||||
if retval and args.show_diff_on_failure and git.has_diff():
|
||||
if retval and args.show_diff_on_failure and prior_diff:
|
||||
if args.all_files:
|
||||
output.write_line(
|
||||
'pre-commit hook(s) made changes.\n'
|
||||
|
|
@ -296,8 +329,7 @@ def _has_unmerged_paths() -> bool:
|
|||
|
||||
def _has_unstaged_config(config_file: str) -> bool:
|
||||
retcode, _, _ = cmd_output_b(
|
||||
'git', 'diff', '--no-ext-diff', '--exit-code', config_file,
|
||||
retcode=None,
|
||||
'git', 'diff', '--quiet', '--no-ext-diff', config_file, check=False,
|
||||
)
|
||||
# be explicit, other git errors don't mean it has an unstaged config.
|
||||
return retcode == 1
|
||||
|
|
@ -307,12 +339,12 @@ def run(
|
|||
config_file: str,
|
||||
store: Store,
|
||||
args: argparse.Namespace,
|
||||
environ: EnvironT = os.environ,
|
||||
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.
|
||||
if _has_unmerged_paths():
|
||||
if stash and _has_unmerged_paths():
|
||||
logger.error('Unmerged files. Resolve before committing.')
|
||||
return 1
|
||||
if bool(args.from_ref) != bool(args.to_ref):
|
||||
|
|
@ -340,6 +372,16 @@ def run(
|
|||
):
|
||||
return 0
|
||||
|
||||
# Expose prepare_commit_message_source / commit_object_name
|
||||
# as environment variables for the hooks
|
||||
if args.prepare_commit_message_source:
|
||||
environ['PRE_COMMIT_COMMIT_MSG_SOURCE'] = (
|
||||
args.prepare_commit_message_source
|
||||
)
|
||||
|
||||
if args.commit_object_name:
|
||||
environ['PRE_COMMIT_COMMIT_OBJECT_NAME'] = args.commit_object_name
|
||||
|
||||
# Expose from-ref / to-ref as environment variables for hooks to consume
|
||||
if args.from_ref and args.to_ref:
|
||||
# legacy names
|
||||
|
|
@ -349,13 +391,28 @@ def run(
|
|||
environ['PRE_COMMIT_FROM_REF'] = args.from_ref
|
||||
environ['PRE_COMMIT_TO_REF'] = args.to_ref
|
||||
|
||||
if args.remote_name and args.remote_url:
|
||||
if args.pre_rebase_upstream and args.pre_rebase_branch:
|
||||
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'
|
||||
|
||||
|
|
@ -377,9 +434,15 @@ def run(
|
|||
)
|
||||
return 1
|
||||
|
||||
install_hook_envs(hooks, store)
|
||||
skips = _get_skips(environ)
|
||||
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(config, hooks, args, environ)
|
||||
return _run_hooks(config, hooks, skips, args)
|
||||
|
||||
# https://github.com/python/mypy/issues/7726
|
||||
raise AssertionError('unreachable')
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
# 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.
|
||||
from __future__ import annotations
|
||||
SAMPLE_CONFIG = '''\
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os.path
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
import tempfile
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import git
|
||||
|
|
@ -11,14 +12,13 @@ from pre_commit.clientlib import load_manifest
|
|||
from pre_commit.commands.run import run
|
||||
from pre_commit.store import Store
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import tmpdir
|
||||
from pre_commit.util import yaml_dump
|
||||
from pre_commit.xargs import xargs
|
||||
from pre_commit.yaml import yaml_dump
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]:
|
||||
def _repo_ref(tmpdir: str, repo: str, ref: str | None) -> tuple[str, str]:
|
||||
# if `ref` is explicitly passed, use it
|
||||
if ref is not None:
|
||||
return repo, ref
|
||||
|
|
@ -49,7 +49,7 @@ def _repo_ref(tmpdir: str, repo: str, ref: Optional[str]) -> Tuple[str, str]:
|
|||
|
||||
|
||||
def try_repo(args: argparse.Namespace) -> int:
|
||||
with tmpdir() as tempdir:
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
repo, ref = _repo_ref(tempdir, args.repo, args.ref)
|
||||
|
||||
store = Store(tempdir)
|
||||
|
|
|
|||
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,24 +1,13 @@
|
|||
import sys
|
||||
from __future__ import annotations
|
||||
|
||||
if sys.version_info < (3, 8): # pragma: no cover (<PY38)
|
||||
import importlib_metadata
|
||||
else: # pragma: no cover (PY38+)
|
||||
import importlib.metadata as importlib_metadata
|
||||
import importlib.metadata
|
||||
|
||||
CONFIG_FILE = '.pre-commit-config.yaml'
|
||||
MANIFEST_FILE = '.pre-commit-hooks.yaml'
|
||||
|
||||
# Bump when installation changes in a backwards / forwards incompatible way
|
||||
INSTALLED_STATE_VERSION = '1'
|
||||
# Bump when modifying `empty_template`
|
||||
LOCAL_REPO_VERSION = '1'
|
||||
|
||||
VERSION = importlib_metadata.version('pre_commit')
|
||||
|
||||
# `manual` is not invoked by any installed git hook. See #719
|
||||
STAGES = (
|
||||
'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
|
||||
'post-commit', 'manual', 'post-checkout', 'push',
|
||||
)
|
||||
VERSION = importlib.metadata.version('pre_commit')
|
||||
|
||||
DEFAULT = 'default'
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import enum
|
||||
import os
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
from collections.abc import MutableMapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
from pre_commit.util import EnvironT
|
||||
|
||||
|
||||
class _Unset(enum.Enum):
|
||||
UNSET = 1
|
||||
|
||||
|
||||
_Unset = enum.Enum('_Unset', 'UNSET')
|
||||
UNSET = _Unset.UNSET
|
||||
|
||||
|
||||
|
|
@ -22,12 +17,12 @@ class Var(NamedTuple):
|
|||
default: str = ''
|
||||
|
||||
|
||||
SubstitutionT = Tuple[Union[str, Var], ...]
|
||||
SubstitutionT = tuple[Union[str, Var], ...]
|
||||
ValueT = Union[str, _Unset, SubstitutionT]
|
||||
PatchesT = Tuple[Tuple[str, ValueT], ...]
|
||||
PatchesT = tuple[tuple[str, ValueT], ...]
|
||||
|
||||
|
||||
def format_env(parts: SubstitutionT, env: EnvironT) -> str:
|
||||
def format_env(parts: SubstitutionT, env: MutableMapping[str, str]) -> str:
|
||||
return ''.join(
|
||||
env.get(part.name, part.default) if isinstance(part, Var) else part
|
||||
for part in parts
|
||||
|
|
@ -37,8 +32,8 @@ def format_env(parts: SubstitutionT, env: EnvironT) -> str:
|
|||
@contextlib.contextmanager
|
||||
def envcontext(
|
||||
patch: PatchesT,
|
||||
_env: Optional[EnvironT] = None,
|
||||
) -> Generator[None, None, None]:
|
||||
_env: MutableMapping[str, str] | None = None,
|
||||
) -> Generator[None]:
|
||||
"""In this context, `os.environ` is modified according to `patch`.
|
||||
|
||||
`patch` is an iterable of 2-tuples (key, value):
|
||||
|
|
@ -50,7 +45,7 @@ def envcontext(
|
|||
replaced with the previous environment
|
||||
"""
|
||||
env = os.environ if _env is None else _env
|
||||
before = env.copy()
|
||||
before = dict(env)
|
||||
|
||||
for k, v in patch:
|
||||
if v is UNSET:
|
||||
|
|
|
|||
|
|
@ -1,27 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os.path
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
from typing import IO
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import output
|
||||
from pre_commit.errors import FatalError
|
||||
from pre_commit.store import Store
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import force_bytes
|
||||
|
||||
|
||||
class FatalError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
|
||||
def _log_and_exit(
|
||||
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)
|
||||
log_path = os.path.join(Store().directory, 'pre-commit.log')
|
||||
output.write_line(f'Check the log at {log_path}')
|
||||
|
||||
with open(log_path, 'wb') as log:
|
||||
_, git_version_b, _ = cmd_output_b('git', '--version', check=False)
|
||||
git_version = git_version_b.decode(errors='backslashreplace').rstrip()
|
||||
|
||||
storedir = Store().directory
|
||||
log_path = os.path.join(storedir, 'pre-commit.log')
|
||||
with contextlib.ExitStack() as ctx:
|
||||
if os.access(storedir, os.W_OK):
|
||||
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)
|
||||
|
||||
|
|
@ -29,6 +45,7 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
|
|||
_log_line()
|
||||
_log_line('```')
|
||||
_log_line(f'pre-commit version: {C.VERSION}')
|
||||
_log_line(f'git --version: {git_version}')
|
||||
_log_line('sys.version:')
|
||||
for line in sys.version.splitlines():
|
||||
_log_line(f' {line}')
|
||||
|
|
@ -45,20 +62,20 @@ def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
|
|||
_log_line('```')
|
||||
_log_line()
|
||||
_log_line('```')
|
||||
_log_line(formatted)
|
||||
_log_line(formatted.rstrip())
|
||||
_log_line('```')
|
||||
raise SystemExit(1)
|
||||
raise SystemExit(ret_code)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def error_handler() -> Generator[None, None, None]:
|
||||
def error_handler() -> Generator[None]:
|
||||
try:
|
||||
yield
|
||||
except (Exception, KeyboardInterrupt) as e:
|
||||
if isinstance(e, FatalError):
|
||||
msg = 'An error has occurred'
|
||||
msg, ret_code = 'An error has occurred', 1
|
||||
elif isinstance(e, KeyboardInterrupt):
|
||||
msg = 'Interrupted (^C)'
|
||||
msg, ret_code = 'Interrupted (^C)', 130
|
||||
else:
|
||||
msg = 'An unexpected error has occurred'
|
||||
_log_and_exit(msg, e, traceback.format_exc())
|
||||
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 annotations
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import os
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
if os.name == 'nt': # pragma: no cover (windows)
|
||||
if sys.platform == 'win32': # pragma: no cover (windows)
|
||||
import msvcrt
|
||||
|
||||
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
|
||||
|
|
@ -18,16 +20,14 @@ if os.name == 'nt': # pragma: no cover (windows)
|
|||
def _locked(
|
||||
fileno: int,
|
||||
blocked_cb: Callable[[], None],
|
||||
) -> Generator[None, None, None]:
|
||||
) -> Generator[None]:
|
||||
try:
|
||||
# TODO: https://github.com/python/typeshed/pull/3607
|
||||
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore
|
||||
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
|
||||
except OSError:
|
||||
blocked_cb()
|
||||
while True:
|
||||
try:
|
||||
# TODO: https://github.com/python/typeshed/pull/3607
|
||||
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501
|
||||
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
|
||||
except OSError as e:
|
||||
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
|
||||
# flag is specified and the file cannot be locked after 10
|
||||
|
|
@ -45,8 +45,7 @@ if os.name == 'nt': # pragma: no cover (windows)
|
|||
# The documentation however states:
|
||||
# "Regions should be locked only briefly and should be unlocked
|
||||
# before closing a file or exiting the program."
|
||||
# TODO: https://github.com/python/typeshed/pull/3607
|
||||
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore
|
||||
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
|
||||
else: # pragma: win32 no cover
|
||||
import fcntl
|
||||
|
||||
|
|
@ -54,7 +53,7 @@ else: # pragma: win32 no cover
|
|||
def _locked(
|
||||
fileno: int,
|
||||
blocked_cb: Callable[[], None],
|
||||
) -> Generator[None, None, None]:
|
||||
) -> Generator[None]:
|
||||
try:
|
||||
fcntl.flock(fileno, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError: # pragma: no cover (tests are single-threaded)
|
||||
|
|
@ -70,7 +69,7 @@ else: # pragma: win32 no cover
|
|||
def lock(
|
||||
path: str,
|
||||
blocked_cb: Callable[[], None],
|
||||
) -> Generator[None, None, None]:
|
||||
) -> Generator[None]:
|
||||
with open(path, 'a+') as f:
|
||||
with _locked(f.fileno(), blocked_cb):
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from collections.abc import Mapping
|
||||
|
||||
from pre_commit.errors import FatalError
|
||||
from pre_commit.util import CalledProcessError
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import EnvironT
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# see #2046
|
||||
NO_FS_MONITOR = ('-c', 'core.useBuiltinFSMonitor=false')
|
||||
|
||||
def zsplit(s: str) -> List[str]:
|
||||
|
||||
def zsplit(s: str) -> list[str]:
|
||||
s = s.strip('\0')
|
||||
if s:
|
||||
return s.split('\0')
|
||||
|
|
@ -22,7 +24,7 @@ def zsplit(s: str) -> List[str]:
|
|||
return []
|
||||
|
||||
|
||||
def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]:
|
||||
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
|
||||
|
|
@ -35,30 +37,60 @@ def no_git_env(_env: Optional[EnvironT] = None) -> Dict[str, str]:
|
|||
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_SSL_NO_VERIFY', 'GIT_CONFIG_COUNT',
|
||||
'GIT_HTTP_PROXY_AUTHMETHOD',
|
||||
'GIT_ALLOW_PROTOCOL',
|
||||
'GIT_ASKPASS',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_root() -> str:
|
||||
return cmd_output('git', 'rev-parse', '--show-toplevel')[1].strip()
|
||||
# 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:
|
||||
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:
|
||||
raise FatalError(
|
||||
'git failed. Is it installed, and are you in a Git repository '
|
||||
'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: str = '.') -> str:
|
||||
opts = ('--git-common-dir', '--git-dir')
|
||||
_, out, _ = cmd_output('git', 'rev-parse', *opts, cwd=git_root)
|
||||
for line, opt in zip(out.splitlines(), opts):
|
||||
if line != opt: # pragma: no branch (git < 2.5)
|
||||
return os.path.normpath(os.path.join(git_root, line))
|
||||
opt = '--git-dir'
|
||||
_, out, _ = cmd_output('git', 'rev-parse', opt, cwd=git_root)
|
||||
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: str) -> str:
|
||||
_, out, _ = cmd_output('git', 'config', 'remote.origin.url', cwd=git_root)
|
||||
return out.strip()
|
||||
def get_git_common_dir(git_root: str = '.') -> str:
|
||||
opt = '--git-common-dir'
|
||||
_, 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() -> bool:
|
||||
|
|
@ -69,7 +101,7 @@ def is_in_merge_conflict() -> bool:
|
|||
)
|
||||
|
||||
|
||||
def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]:
|
||||
def parse_merge_msg_for_conflicts(merge_msg: bytes) -> list[str]:
|
||||
# Conflicted files start with tabs
|
||||
return [
|
||||
line.lstrip(b'#').strip().decode()
|
||||
|
|
@ -79,7 +111,7 @@ def parse_merge_msg_for_conflicts(merge_msg: bytes) -> List[str]:
|
|||
]
|
||||
|
||||
|
||||
def get_conflicted_files() -> Set[str]:
|
||||
def get_conflicted_files() -> set[str]:
|
||||
logger.info('Checking merge-conflict files only.')
|
||||
# Need to get the conflicted files from the MERGE_MSG because they could
|
||||
# have resolved the conflict by choosing one side or the other
|
||||
|
|
@ -94,13 +126,13 @@ def get_conflicted_files() -> Set[str]:
|
|||
merge_diff_filenames = zsplit(
|
||||
cmd_output(
|
||||
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
|
||||
'-m', tree_hash, 'HEAD', 'MERGE_HEAD',
|
||||
'-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
|
||||
)[1],
|
||||
)
|
||||
return set(merge_conflict_filenames) | set(merge_diff_filenames)
|
||||
|
||||
|
||||
def get_staged_files(cwd: Optional[str] = None) -> List[str]:
|
||||
def get_staged_files(cwd: str | None = None) -> list[str]:
|
||||
return zsplit(
|
||||
cmd_output(
|
||||
'git', 'diff', '--staged', '--name-only', '--no-ext-diff', '-z',
|
||||
|
|
@ -111,31 +143,28 @@ def get_staged_files(cwd: Optional[str] = None) -> List[str]:
|
|||
)
|
||||
|
||||
|
||||
def intent_to_add_files() -> List[str]:
|
||||
_, stdout, _ = cmd_output('git', 'status', '--porcelain', '-z')
|
||||
parts = list(reversed(zsplit(stdout)))
|
||||
intent_to_add = []
|
||||
while parts:
|
||||
line = parts.pop()
|
||||
status, filename = line[:3], line[3:]
|
||||
if status[0] in {'C', 'R'}: # renames / moves have an additional arg
|
||||
parts.pop()
|
||||
if status[1] == 'A':
|
||||
intent_to_add.append(filename)
|
||||
return intent_to_add
|
||||
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]:
|
||||
def get_all_files() -> list[str]:
|
||||
return zsplit(cmd_output('git', 'ls-files', '-z')[1])
|
||||
|
||||
|
||||
def get_changed_files(old: str, new: str) -> List[str]:
|
||||
return zsplit(
|
||||
cmd_output(
|
||||
'git', 'diff', '--name-only', '--no-ext-diff', '-z',
|
||||
f'{old}...{new}',
|
||||
)[1],
|
||||
)
|
||||
def get_changed_files(old: str, new: str) -> list[str]:
|
||||
diff_cmd = ('git', 'diff', '--name-only', '--no-ext-diff', '-z')
|
||||
try:
|
||||
_, out, _ = cmd_output(*diff_cmd, f'{old}...{new}')
|
||||
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: str) -> str:
|
||||
|
|
@ -145,11 +174,11 @@ def head_rev(remote: str) -> str:
|
|||
|
||||
def has_diff(*args: str, repo: str = '.') -> bool:
|
||||
cmd = ('git', 'diff', '--quiet', '--no-ext-diff', *args)
|
||||
return cmd_output_b(*cmd, cwd=repo, retcode=None)[0] == 1
|
||||
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', retcode=None)
|
||||
_, out, _ = cmd_output_b('git', 'config', 'core.hooksPath', check=False)
|
||||
return bool(out.strip())
|
||||
|
||||
|
||||
|
|
@ -157,9 +186,11 @@ 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()
|
||||
cmd_output_b('git', 'init', path, env=env)
|
||||
cmd_output_b('git', 'remote', 'add', 'origin', remote, cwd=path, env=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:
|
||||
|
|
@ -180,12 +211,15 @@ def check_for_cygwin_mismatch() -> None:
|
|||
"""See https://github.com/pre-commit/pre-commit/issues/354"""
|
||||
if sys.platform in ('cygwin', 'win32'): # pragma: no cover (windows)
|
||||
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('/')
|
||||
|
||||
if is_cygwin_python ^ is_cygwin_git:
|
||||
exe_type = {True: '(cygwin)', False: '(windows)'}
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f'pre-commit has detected a mix of cygwin python / git\n'
|
||||
f'This combination is not supported, it is likely you will '
|
||||
f'receive an error later in the program.\n'
|
||||
|
|
@ -194,3 +228,18 @@ def check_for_cygwin_mismatch() -> None:
|
|||
f' - python {exe_type[is_cygwin_python]}\n'
|
||||
f' - git {exe_type[is_cygwin_git]}\n',
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shlex
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import NamedTuple
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pre_commit.prefix import Prefix
|
||||
|
||||
|
|
@ -22,10 +21,12 @@ class Hook(NamedTuple):
|
|||
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
|
||||
|
|
@ -36,11 +37,7 @@ class Hook(NamedTuple):
|
|||
verbose: bool
|
||||
|
||||
@property
|
||||
def cmd(self) -> Tuple[str, ...]:
|
||||
return (*shlex.split(self.entry), *self.args)
|
||||
|
||||
@property
|
||||
def install_key(self) -> Tuple[Prefix, str, str, Tuple[str, ...]]:
|
||||
def install_key(self) -> tuple[Prefix, str, str, tuple[str, ...]]:
|
||||
return (
|
||||
self.prefix,
|
||||
self.language,
|
||||
|
|
@ -49,7 +46,7 @@ class Hook(NamedTuple):
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, src: str, prefix: Prefix, dct: Dict[str, Any]) -> 'Hook':
|
||||
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:
|
||||
|
|
|
|||
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,60 +0,0 @@
|
|||
from typing import Callable
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import conda
|
||||
from pre_commit.languages import docker
|
||||
from pre_commit.languages import docker_image
|
||||
from pre_commit.languages import fail
|
||||
from pre_commit.languages import golang
|
||||
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 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
|
||||
from pre_commit.prefix import Prefix
|
||||
|
||||
|
||||
class Language(NamedTuple):
|
||||
name: str
|
||||
# Use `None` for no installation / environment
|
||||
ENVIRONMENT_DIR: Optional[str]
|
||||
# return a value to replace `'default` for `language_version`
|
||||
get_default_version: Callable[[], str]
|
||||
# return whether the environment is healthy (or should be rebuilt)
|
||||
healthy: Callable[[Prefix, str], bool]
|
||||
# install a repository for the given language and language_version
|
||||
install_environment: Callable[[Prefix, str, Sequence[str]], None]
|
||||
# execute a hook and return the exit code and output
|
||||
run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]'
|
||||
|
||||
|
||||
# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018
|
||||
languages = {
|
||||
# BEGIN GENERATED (testing/gen-languages-all)
|
||||
'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501
|
||||
'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501
|
||||
'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501
|
||||
'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501
|
||||
'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501
|
||||
'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501
|
||||
'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
|
||||
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
|
||||
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
|
||||
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
|
||||
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
|
||||
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
|
||||
'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501
|
||||
'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
|
||||
# END GENERATED
|
||||
}
|
||||
# TODO: fully deprecate `python_venv`
|
||||
languages['python_venv'] = languages['python']
|
||||
all_languages = sorted(languages)
|
||||
|
|
@ -1,23 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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.hook import Hook
|
||||
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
|
||||
|
||||
ENVIRONMENT_DIR = 'conda'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
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:
|
||||
|
|
@ -26,7 +27,7 @@ def get_env_patch(env: str) -> PatchesT:
|
|||
# $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 os.name == 'nt': # pragma: no cover (platform specific)
|
||||
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)
|
||||
|
|
@ -40,45 +41,37 @@ def get_env_patch(env: str) -> PatchesT:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_env(
|
||||
prefix: Prefix,
|
||||
language_version: str,
|
||||
) -> Generator[None, None, None]:
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
|
||||
envdir = prefix.path(directory)
|
||||
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:
|
||||
helpers.assert_version_default('conda', version)
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
||||
lang_base.assert_version_default('conda', version)
|
||||
|
||||
env_dir = prefix.path(directory)
|
||||
with clean_path_on_failure(env_dir):
|
||||
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', 'env', 'create', '-p', env_dir, '--file',
|
||||
'environment.yml', cwd=prefix.prefix_dir,
|
||||
conda_exe, 'install', '-p', env_dir, *additional_dependencies,
|
||||
cwd=prefix.prefix_dir,
|
||||
)
|
||||
if additional_dependencies:
|
||||
cmd_output_b(
|
||||
'conda', 'install', '-p', env_dir, *additional_dependencies,
|
||||
cwd=prefix.prefix_dir,
|
||||
)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
# TODO: Some rare commands need to be run using `conda run` but mostly we
|
||||
# can run them withot which is much quicker and produces a better
|
||||
# output.
|
||||
# cmd = ('conda', 'run', '-p', env_dir) + hook.cmd
|
||||
with in_env(hook.prefix, hook.language_version):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
|
|
|
|||
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,20 +1,69 @@
|
|||
import hashlib
|
||||
import os
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
import contextlib
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pre_commit import lang_base
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import CalledProcessError
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
from pre_commit.util import cmd_output_b
|
||||
|
||||
ENVIRONMENT_DIR = 'docker'
|
||||
PRE_COMMIT_LABEL = 'PRE_COMMIT'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
get_default_version = lang_base.basic_get_default_version
|
||||
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 _get_container_id() -> str | None:
|
||||
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 _get_docker_path(path: str) -> str:
|
||||
container_id = _get_container_id()
|
||||
if container_id is None:
|
||||
return path
|
||||
|
||||
try:
|
||||
_, out, _ = cmd_output_b('docker', 'inspect', container_id)
|
||||
except CalledProcessError:
|
||||
# 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 md5(s: str) -> str: # pragma: win32 no cover
|
||||
|
|
@ -26,27 +75,12 @@ def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover
|
|||
return f'pre-commit-{md5sum}'
|
||||
|
||||
|
||||
def docker_is_running() -> bool: # pragma: win32 no cover
|
||||
try:
|
||||
cmd_output_b('docker', 'ps')
|
||||
except CalledProcessError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def assert_docker_available() -> None: # pragma: win32 no cover
|
||||
assert docker_is_running(), (
|
||||
'Docker is either not running or not configured in this environment'
|
||||
)
|
||||
|
||||
|
||||
def build_docker_image(
|
||||
prefix: Prefix,
|
||||
*,
|
||||
pull: bool,
|
||||
) -> None: # pragma: win32 no cover
|
||||
cmd: Tuple[str, ...] = (
|
||||
cmd: tuple[str, ...] = (
|
||||
'docker', 'build',
|
||||
'--tag', docker_tag(prefix),
|
||||
'--label', PRE_COMMIT_LABEL,
|
||||
|
|
@ -55,60 +89,93 @@ def build_docker_image(
|
|||
cmd += ('--pull',)
|
||||
# This must come last for old versions of docker. See #477
|
||||
cmd += ('.',)
|
||||
helpers.run_setup_cmd(prefix, cmd)
|
||||
lang_base.setup_cmd(prefix, cmd)
|
||||
|
||||
|
||||
def install_environment(
|
||||
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||
) -> None: # pragma: win32 no cover
|
||||
helpers.assert_version_default('docker', version)
|
||||
helpers.assert_no_additional_deps('docker', additional_dependencies)
|
||||
assert_docker_available()
|
||||
lang_base.assert_version_default('docker', version)
|
||||
lang_base.assert_no_additional_deps('docker', additional_dependencies)
|
||||
|
||||
directory = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
directory = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
# Docker doesn't really have relevant disk environment, but pre-commit
|
||||
# still needs to cleanup its state files on failure
|
||||
with clean_path_on_failure(directory):
|
||||
build_docker_image(prefix, pull=True)
|
||||
os.mkdir(directory)
|
||||
build_docker_image(prefix, pull=True)
|
||||
os.mkdir(directory)
|
||||
|
||||
|
||||
def get_docker_user() -> Tuple[str, ...]: # pragma: win32 no cover
|
||||
@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 docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover
|
||||
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 (
|
||||
'docker', 'run',
|
||||
'--rm',
|
||||
*get_docker_tty(color=color),
|
||||
*get_docker_user(),
|
||||
# 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
|
||||
# unshared label. Only the current container can use a private volume.
|
||||
'-v', f'{os.getcwd()}:/src:rw,Z',
|
||||
'-v', f'{_get_docker_path(os.getcwd())}:/src:rw,Z',
|
||||
'--workdir', '/src',
|
||||
)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
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
|
||||
assert_docker_available()
|
||||
) -> tuple[int, bytes]: # pragma: win32 no cover
|
||||
# Rebuild the docker image in case it has gone missing, as many people do
|
||||
# automated cleanup of docker images.
|
||||
build_docker_image(hook.prefix, pull=False)
|
||||
build_docker_image(prefix, pull=False)
|
||||
|
||||
hook_cmd = hook.cmd
|
||||
entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:]
|
||||
entry_exe, *cmd_rest = lang_base.hook_cmd(entry, args)
|
||||
|
||||
entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix))
|
||||
cmd = docker_cmd() + entry_tag + cmd_rest
|
||||
return helpers.run_xargs(hook, cmd, file_args, color=color)
|
||||
entry_tag = ('--entrypoint', entry_exe, docker_tag(prefix))
|
||||
return lang_base.run_xargs(
|
||||
(*docker_cmd(color=color), *entry_tag, *cmd_rest),
|
||||
file_args,
|
||||
require_serial=require_serial,
|
||||
color=color,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,22 +1,32 @@
|
|||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.languages.docker import assert_docker_available
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pre_commit import lang_base
|
||||
from pre_commit.languages.docker import docker_cmd
|
||||
from pre_commit.prefix import Prefix
|
||||
|
||||
ENVIRONMENT_DIR = None
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
install_environment = helpers.no_install
|
||||
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(
|
||||
hook: Hook,
|
||||
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
|
||||
assert_docker_available()
|
||||
cmd = docker_cmd() + hook.cmd
|
||||
return helpers.run_xargs(hook, cmd, file_args, color=color)
|
||||
) -> 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,
|
||||
),
|
||||
)
|
||||
|
|
@ -1,20 +1,27 @@
|
|||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pre_commit import lang_base
|
||||
from pre_commit.prefix import Prefix
|
||||
|
||||
ENVIRONMENT_DIR = None
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
install_environment = helpers.no_install
|
||||
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(
|
||||
hook: 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'{hook.entry}\n\n'.encode()
|
||||
) -> 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,97 +1,161 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import json
|
||||
import os.path
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import git
|
||||
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.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.git import no_git_env
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import rmtree
|
||||
|
||||
ENVIRONMENT_DIR = 'golangenv'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
health_check = lang_base.basic_health_check
|
||||
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: str) -> PatchesT:
|
||||
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 (
|
||||
('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
|
||||
def in_env(prefix: Prefix) -> Generator[None, None, None]:
|
||||
envdir = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
with envcontext(get_env_patch(envdir)):
|
||||
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 guess_go_dir(remote_url: str) -> str:
|
||||
if remote_url.endswith('.git'):
|
||||
remote_url = remote_url[:-1 * len('.git')]
|
||||
looks_like_url = (
|
||||
not remote_url.startswith('file://') and
|
||||
('//' in remote_url or '@' in remote_url)
|
||||
)
|
||||
remote_url = remote_url.replace(':', '/')
|
||||
if looks_like_url:
|
||||
_, _, remote_url = remote_url.rpartition('//')
|
||||
_, _, remote_url = remote_url.rpartition('@')
|
||||
return remote_url
|
||||
else:
|
||||
return 'unknown_src_dir'
|
||||
|
||||
|
||||
def install_environment(
|
||||
prefix: Prefix,
|
||||
version: str,
|
||||
additional_dependencies: Sequence[str],
|
||||
) -> None:
|
||||
helpers.assert_version_default('golang', version)
|
||||
directory = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
with clean_path_on_failure(directory):
|
||||
remote = git.get_remote_url(prefix.prefix_dir)
|
||||
repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote))
|
||||
if version != 'system':
|
||||
_install_go(version, env_dir)
|
||||
|
||||
# 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', env_dir)[1].strip()
|
||||
else:
|
||||
gopath = env_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)
|
||||
env.pop('GOBIN', None)
|
||||
cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env)
|
||||
for dependency in additional_dependencies:
|
||||
cmd_output_b('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)
|
||||
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'],
|
||||
))
|
||||
|
||||
lang_base.setup_cmd(prefix, ('go', 'install', './...'), env=env)
|
||||
for dependency in additional_dependencies:
|
||||
lang_base.setup_cmd(prefix, ('go', 'install', dependency), env=env)
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
with in_env(hook.prefix):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
# save some disk space -- we don't need this after installation
|
||||
pkgdir = os.path.join(env_dir, 'pkg')
|
||||
if os.path.exists(pkgdir): # pragma: no branch (always true on windows?)
|
||||
rmtree(pkgdir)
|
||||
|
|
|
|||
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,109 +0,0 @@
|
|||
import multiprocessing
|
||||
import os
|
||||
import random
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.xargs import xargs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
|
||||
FIXED_RANDOM_SEED = 1542676187
|
||||
|
||||
|
||||
def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None:
|
||||
cmd_output_b(*cmd, cwd=prefix.prefix_dir)
|
||||
|
||||
|
||||
@overload
|
||||
def environment_dir(d: None, language_version: str) -> None: ...
|
||||
@overload
|
||||
def environment_dir(d: str, language_version: str) -> str: ...
|
||||
|
||||
|
||||
def environment_dir(d: Optional[str], language_version: str) -> Optional[str]:
|
||||
if d is None:
|
||||
return None
|
||||
else:
|
||||
return 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}',
|
||||
)
|
||||
|
||||
|
||||
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}',
|
||||
)
|
||||
|
||||
|
||||
def basic_get_default_version() -> str:
|
||||
return C.DEFAULT
|
||||
|
||||
|
||||
def basic_healthy(prefix: Prefix, language_version: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def no_install(
|
||||
prefix: Prefix,
|
||||
version: str,
|
||||
additional_dependencies: Sequence[str],
|
||||
) -> 'NoReturn':
|
||||
raise AssertionError('This type is not installable')
|
||||
|
||||
|
||||
def target_concurrency(hook: Hook) -> int:
|
||||
if hook.require_serial or '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:
|
||||
try:
|
||||
return multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
return 1
|
||||
|
||||
|
||||
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(
|
||||
hook: Hook,
|
||||
cmd: Tuple[str, ...],
|
||||
file_args: Sequence[str],
|
||||
**kwargs: Any,
|
||||
) -> Tuple[int, bytes]:
|
||||
# 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)
|
||||
kwargs['target_concurrency'] = target_concurrency(hook)
|
||||
return xargs(cmd, file_args, **kwargs)
|
||||
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,26 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import parse_shebang
|
||||
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.envcontext import Var
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.languages.python import bin_dir
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import rmtree
|
||||
|
||||
ENVIRONMENT_DIR = 'node_env'
|
||||
healthy = helpers.basic_healthy
|
||||
run_hook = lang_base.basic_run_hook
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
|
|
@ -30,17 +30,12 @@ def get_default_version() -> str:
|
|||
return C.DEFAULT
|
||||
# if node is already installed, we can save a bunch of setup time by
|
||||
# using the installed version
|
||||
elif all(parse_shebang.find_executable(exe) for exe in ('node', 'npm')):
|
||||
elif all(lang_base.exe_exists(exe) for exe in ('node', 'npm')):
|
||||
return 'system'
|
||||
else:
|
||||
return C.DEFAULT
|
||||
|
||||
|
||||
def _envdir(prefix: Prefix, version: str) -> str:
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
||||
return prefix.path(directory)
|
||||
|
||||
|
||||
def get_env_patch(venv: str) -> PatchesT:
|
||||
if sys.platform == 'cygwin': # pragma: no cover
|
||||
_, win_venv, _ = cmd_output('cygpath', '-w', venv)
|
||||
|
|
@ -56,52 +51,60 @@ def get_env_patch(venv: str) -> PatchesT:
|
|||
('NODE_VIRTUAL_ENV', venv),
|
||||
('NPM_CONFIG_PREFIX', install_prefix),
|
||||
('npm_config_prefix', install_prefix),
|
||||
('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'))),
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_env(
|
||||
prefix: Prefix,
|
||||
language_version: str,
|
||||
) -> Generator[None, None, None]:
|
||||
with envcontext(get_env_patch(_envdir(prefix, language_version))):
|
||||
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 health_check(prefix: Prefix, version: str) -> str | None:
|
||||
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:
|
||||
additional_dependencies = tuple(additional_dependencies)
|
||||
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
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
envdir = fr'\\?\{os.path.normpath(envdir)}'
|
||||
with clean_path_on_failure(envdir):
|
||||
cmd = [
|
||||
sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir,
|
||||
]
|
||||
if version != C.DEFAULT:
|
||||
cmd.extend(['-n', version])
|
||||
cmd_output_b(*cmd)
|
||||
cmd = [sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir]
|
||||
if version != C.DEFAULT:
|
||||
cmd.extend(['-n', version])
|
||||
cmd_output_b(*cmd)
|
||||
|
||||
with in_env(prefix, version):
|
||||
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
|
||||
# install as if we installed from git
|
||||
helpers.run_setup_cmd(prefix, ('npm', 'install'))
|
||||
helpers.run_setup_cmd(
|
||||
prefix,
|
||||
('npm', 'install', '-g', '.', *additional_dependencies),
|
||||
)
|
||||
with in_env(prefix, version):
|
||||
# https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449
|
||||
# install as if we installed from git
|
||||
|
||||
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(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
with in_env(hook.prefix, hook.language_version):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
_, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir)
|
||||
pkg = prefix.path(pkg.strip())
|
||||
|
||||
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,26 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import shlex
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
|
||||
ENVIRONMENT_DIR = 'perl_env'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
|
||||
|
||||
def _envdir(prefix: Prefix, version: str) -> str:
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
||||
return prefix.path(directory)
|
||||
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:
|
||||
|
|
@ -38,30 +33,18 @@ def get_env_patch(venv: str) -> PatchesT:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_env(
|
||||
prefix: Prefix,
|
||||
language_version: str,
|
||||
) -> Generator[None, None, None]:
|
||||
with envcontext(get_env_patch(_envdir(prefix, language_version))):
|
||||
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:
|
||||
helpers.assert_version_default('perl', version)
|
||||
lang_base.assert_version_default('perl', version)
|
||||
|
||||
with clean_path_on_failure(_envdir(prefix, version)):
|
||||
with in_env(prefix, version):
|
||||
helpers.run_setup_cmd(
|
||||
prefix, ('cpan', '-T', '.', *additional_dependencies),
|
||||
)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
with in_env(hook.prefix, hook.language_version):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
with in_env(prefix, version):
|
||||
lang_base.setup_cmd(
|
||||
prefix, ('cpan', '-T', '.', *additional_dependencies),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.prefix import Prefix
|
||||
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
|
||||
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 _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
|
||||
|
|
@ -45,16 +47,61 @@ def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
|
|||
return retv
|
||||
|
||||
|
||||
def _process_filename_by_line_negated(
|
||||
pattern: Pattern[bytes],
|
||||
filename: str,
|
||||
) -> 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 _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(
|
||||
hook: Hook,
|
||||
prefix: Prefix,
|
||||
entry: str,
|
||||
args: Sequence[str],
|
||||
file_args: Sequence[str],
|
||||
*,
|
||||
is_local: bool,
|
||||
require_serial: bool,
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,)
|
||||
return xargs(exe, file_args, color=color)
|
||||
) -> tuple[int, bytes]:
|
||||
cmd = (sys.executable, '-m', __name__, *args, entry)
|
||||
return xargs(cmd, file_args, color=color)
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
'grep-like finder using python regexes. Unlike grep, this tool '
|
||||
|
|
@ -64,6 +111,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
)
|
||||
parser.add_argument('-i', '--ignore-case', 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('filenames', nargs='*')
|
||||
args = parser.parse_args(argv)
|
||||
|
|
@ -75,13 +123,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
pattern = re.compile(args.pattern.encode(), flags)
|
||||
|
||||
retv = 0
|
||||
process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)]
|
||||
for filename in args.filenames:
|
||||
if args.multiline:
|
||||
retv |= _process_filename_at_once(pattern, filename)
|
||||
else:
|
||||
retv |= _process_filename_by_line(pattern, filename)
|
||||
retv |= process_fn(pattern, filename)
|
||||
return retv
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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 PatchesT
|
||||
from pre_commit.envcontext import UNSET
|
||||
from pre_commit.envcontext import Var
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
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 clean_path_on_failure
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import win_exe
|
||||
|
||||
ENVIRONMENT_DIR = 'py_env'
|
||||
run_hook = lang_base.basic_run_hook
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.cache
|
||||
def _version_info(exe: str) -> str:
|
||||
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
|
||||
try:
|
||||
|
|
@ -34,9 +33,9 @@ def _version_info(exe: str) -> str:
|
|||
return f'<<error retrieving version from {exe}>>'
|
||||
|
||||
|
||||
def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
|
||||
def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
|
||||
ret = {}
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding='UTF-8') as f:
|
||||
for line in f:
|
||||
try:
|
||||
k, v = line.split('=')
|
||||
|
|
@ -49,7 +48,7 @@ def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
|
|||
|
||||
def bin_dir(venv: str) -> str:
|
||||
"""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)
|
||||
|
||||
|
||||
|
|
@ -64,9 +63,9 @@ def get_env_patch(venv: str) -> PatchesT:
|
|||
|
||||
def _find_by_py_launcher(
|
||||
version: str,
|
||||
) -> Optional[str]: # pragma: no cover (windows only)
|
||||
) -> str | None: # pragma: no cover (windows only)
|
||||
if version.startswith('python'):
|
||||
num = version[len('python'):]
|
||||
num = version.removeprefix('python')
|
||||
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
|
||||
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
|
||||
try:
|
||||
|
|
@ -76,8 +75,15 @@ def _find_by_py_launcher(
|
|||
return None
|
||||
|
||||
|
||||
def _find_by_sys_executable() -> Optional[str]:
|
||||
def _norm(path: str) -> Optional[str]:
|
||||
def _impl_exe_name() -> str:
|
||||
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, _, _ = exe.partition('.exe')
|
||||
if exe not in {'python', 'pythonw'} and find_executable(exe):
|
||||
|
|
@ -101,23 +107,25 @@ def _find_by_sys_executable() -> Optional[str]:
|
|||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def get_default_version() -> str: # pragma: no cover (platform dependent)
|
||||
# First attempt from `sys.executable` (or the realpath)
|
||||
exe = _find_by_sys_executable()
|
||||
if exe:
|
||||
return exe
|
||||
v_major = f'{sys.version_info[0]}'
|
||||
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
|
||||
|
||||
# Next try the `pythonX.X` executable
|
||||
exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
|
||||
if find_executable(exe):
|
||||
return exe
|
||||
# attempt the likely implementation exe
|
||||
for potential in (v_minor, v_major):
|
||||
exe = f'{_impl_exe_name()}{potential}'
|
||||
if find_executable(exe):
|
||||
return exe
|
||||
|
||||
if _find_by_py_launcher(exe):
|
||||
return exe
|
||||
# next try `sys.executable` (or the realpath)
|
||||
maybe_exe = _find_by_sys_executable()
|
||||
if maybe_exe:
|
||||
return maybe_exe
|
||||
|
||||
# Give a best-effort try for windows
|
||||
default_folder_name = exe.replace('.', '')
|
||||
if os.path.exists(fr'C:\{default_folder_name}\python.exe'):
|
||||
return 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!
|
||||
return C.DEFAULT
|
||||
|
|
@ -130,22 +138,20 @@ def _sys_executable_matches(version: str) -> bool:
|
|||
return False
|
||||
|
||||
try:
|
||||
info = tuple(int(p) for p in version[len('python'):].split('.'))
|
||||
info = tuple(int(p) for p in version.removeprefix('python').split('.'))
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return sys.version_info[:len(info)] == info
|
||||
|
||||
|
||||
def norm_version(version: str) -> str:
|
||||
if version == C.DEFAULT:
|
||||
return os.path.realpath(sys.executable)
|
||||
def norm_version(version: str) -> str | None:
|
||||
if version == C.DEFAULT: # use virtualenv's default
|
||||
return None
|
||||
elif _sys_executable_matches(version): # virtualenv defaults to our exe
|
||||
return None
|
||||
|
||||
# first see if our current executable is appropriate
|
||||
if _sys_executable_matches(version):
|
||||
return sys.executable
|
||||
|
||||
if os.name == 'nt': # pragma: no cover (windows)
|
||||
if sys.platform == 'win32': # pragma: no cover (windows)
|
||||
version_exec = _find_by_py_launcher(version)
|
||||
if version_exec:
|
||||
return version_exec
|
||||
|
|
@ -155,47 +161,54 @@ def norm_version(version: str) -> str:
|
|||
if version_exec and version_exec != version:
|
||||
return version_exec
|
||||
|
||||
# If it is in the form pythonx.x search in the default
|
||||
# place on windows
|
||||
if version.startswith('python'):
|
||||
default_folder_name = version.replace('.', '')
|
||||
return fr'C:\{default_folder_name}\python.exe'
|
||||
|
||||
# Otherwise assume it is a path
|
||||
return os.path.expanduser(version)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_env(
|
||||
prefix: Prefix,
|
||||
language_version: str,
|
||||
) -> Generator[None, None, None]:
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
|
||||
envdir = prefix.path(directory)
|
||||
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 healthy(prefix: Prefix, language_version: str) -> bool:
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
|
||||
envdir = prefix.path(directory)
|
||||
def health_check(prefix: Prefix, version: str) -> str | None:
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
|
||||
|
||||
# created with "old" virtualenv
|
||||
if not os.path.exists(pyvenv_cfg):
|
||||
return False
|
||||
return 'pyvenv.cfg does not exist (old virtualenv?)'
|
||||
|
||||
exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
|
||||
exe_name = win_exe('python')
|
||||
py_exe = prefix.path(bin_dir(envdir), exe_name)
|
||||
cfg = _read_pyvenv_cfg(pyvenv_cfg)
|
||||
|
||||
return (
|
||||
'version_info' in cfg and
|
||||
_version_info(py_exe) == cfg['version_info'] and (
|
||||
'base-executable' not in cfg or
|
||||
_version_info(cfg['base-executable']) == cfg['version_info']
|
||||
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
|
||||
|
||||
|
||||
def install_environment(
|
||||
|
|
@ -203,21 +216,13 @@ def install_environment(
|
|||
version: str,
|
||||
additional_dependencies: Sequence[str],
|
||||
) -> None:
|
||||
envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
venv_cmd = [sys.executable, '-mvirtualenv', envdir]
|
||||
python = norm_version(version)
|
||||
venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
|
||||
if python is not None:
|
||||
venv_cmd.extend(('-p', python))
|
||||
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
|
||||
|
||||
with clean_path_on_failure(envdir):
|
||||
cmd_output_b(*venv_cmd, cwd='/')
|
||||
with in_env(prefix, version):
|
||||
helpers.run_setup_cmd(prefix, install_cmd)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
with in_env(hook.prefix, hook.language_version):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
cmd_output_b(*venv_cmd, cwd='/')
|
||||
with in_env(prefix, version):
|
||||
lang_base.setup_cmd(prefix, install_cmd)
|
||||
|
|
|
|||
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,80 +1,102 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import importlib.resources
|
||||
import os.path
|
||||
import shutil
|
||||
import tarfile
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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 PatchesT
|
||||
from pre_commit.envcontext import UNSET
|
||||
from pre_commit.envcontext import Var
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.util import CalledProcessError
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
from pre_commit.util import resource_bytesio
|
||||
|
||||
ENVIRONMENT_DIR = 'rbenv'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
health_check = lang_base.basic_health_check
|
||||
run_hook = lang_base.basic_run_hook
|
||||
|
||||
|
||||
def _resource_bytesio(filename: str) -> IO[bytes]:
|
||||
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: # pragma: win32 no cover
|
||||
) -> PatchesT:
|
||||
patches: PatchesT = (
|
||||
('GEM_HOME', os.path.join(venv, 'gems')),
|
||||
('GEM_PATH', UNSET),
|
||||
('RBENV_ROOT', venv),
|
||||
('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 != C.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),)
|
||||
|
||||
return patches
|
||||
|
||||
|
||||
@contextlib.contextmanager # pragma: win32 no cover
|
||||
def in_env(
|
||||
prefix: Prefix,
|
||||
language_version: str,
|
||||
) -> Generator[None, None, None]:
|
||||
envdir = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, language_version),
|
||||
)
|
||||
with envcontext(get_env_patch(envdir, language_version)):
|
||||
@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 _extract_resource(filename: str, dest: str) -> None:
|
||||
with resource_bytesio(filename) as bio:
|
||||
with _resource_bytesio(filename) as bio:
|
||||
with tarfile.open(fileobj=bio) as tf:
|
||||
tf.extractall(dest)
|
||||
|
||||
|
||||
def _install_rbenv(
|
||||
prefix: Prefix,
|
||||
version: str = C.DEFAULT,
|
||||
version: str,
|
||||
) -> None: # pragma: win32 no cover
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
_extract_resource('rbenv.tar.gz', prefix.path('.'))
|
||||
shutil.move(prefix.path('rbenv'), prefix.path(directory))
|
||||
shutil.move(prefix.path('rbenv'), envdir)
|
||||
|
||||
# Only install ruby-build if the version is specified
|
||||
if version != C.DEFAULT:
|
||||
plugins_dir = prefix.path(directory, 'plugins')
|
||||
plugins_dir = os.path.join(envdir, 'plugins')
|
||||
_extract_resource('ruby-download.tar.gz', plugins_dir)
|
||||
_extract_resource('ruby-build.tar.gz', plugins_dir)
|
||||
|
||||
|
|
@ -84,45 +106,40 @@ def _install_ruby(
|
|||
version: str,
|
||||
) -> None: # pragma: win32 no cover
|
||||
try:
|
||||
helpers.run_setup_cmd(prefix, ('rbenv', 'download', version))
|
||||
lang_base.setup_cmd(prefix, ('rbenv', 'download', version))
|
||||
except CalledProcessError: # pragma: no cover (usually find with download)
|
||||
# Failed to download from mirror for some reason, build it instead
|
||||
helpers.run_setup_cmd(prefix, ('rbenv', 'install', version))
|
||||
lang_base.setup_cmd(prefix, ('rbenv', 'install', version))
|
||||
|
||||
|
||||
def install_environment(
|
||||
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||
) -> None: # pragma: win32 no cover
|
||||
additional_dependencies = tuple(additional_dependencies)
|
||||
directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
|
||||
with clean_path_on_failure(prefix.path(directory)):
|
||||
# TODO: this currently will fail if there's no version specified and
|
||||
# there's no system ruby installed. Is this ok?
|
||||
_install_rbenv(prefix, version=version)
|
||||
) -> None:
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
if version != 'system': # pragma: win32 no cover
|
||||
_install_rbenv(prefix, version)
|
||||
with in_env(prefix, version):
|
||||
# Need to call this before installing so rbenv's directories are
|
||||
# set up
|
||||
helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-'))
|
||||
# Need to call this before installing so rbenv's directories
|
||||
# are set up
|
||||
lang_base.setup_cmd(prefix, ('rbenv', 'init', '-'))
|
||||
if version != C.DEFAULT:
|
||||
_install_ruby(prefix, version)
|
||||
# Need to call this after installing to set up the shims
|
||||
helpers.run_setup_cmd(prefix, ('rbenv', 'rehash'))
|
||||
helpers.run_setup_cmd(
|
||||
prefix, ('gem', 'build', *prefix.star('.gemspec')),
|
||||
)
|
||||
helpers.run_setup_cmd(
|
||||
prefix,
|
||||
(
|
||||
'gem', 'install', '--no-document',
|
||||
*prefix.star('.gem'), *additional_dependencies,
|
||||
),
|
||||
)
|
||||
lang_base.setup_cmd(prefix, ('rbenv', 'rehash'))
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]: # pragma: win32 no cover
|
||||
with in_env(hook.prefix, hook.language_version):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
with in_env(prefix, version):
|
||||
lang_base.setup_cmd(
|
||||
prefix, ('gem', 'build', *prefix.star('.gemspec')),
|
||||
)
|
||||
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,55 +1,113 @@
|
|||
import contextlib
|
||||
import os.path
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from __future__ import annotations
|
||||
|
||||
import toml
|
||||
import contextlib
|
||||
import functools
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.request
|
||||
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 import parse_shebang
|
||||
from pre_commit.envcontext import envcontext
|
||||
from pre_commit.envcontext import PatchesT
|
||||
from pre_commit.envcontext import Var
|
||||
from pre_commit.hook import Hook
|
||||
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 make_executable
|
||||
from pre_commit.util import win_exe
|
||||
|
||||
ENVIRONMENT_DIR = 'rustenv'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
health_check = lang_base.basic_health_check
|
||||
run_hook = lang_base.basic_run_hook
|
||||
|
||||
|
||||
def get_env_patch(target_dir: str) -> PatchesT:
|
||||
@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 (
|
||||
('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))),
|
||||
# Only set RUSTUP_TOOLCHAIN if we don't want use the system's default
|
||||
# toolchain
|
||||
*(
|
||||
(('RUSTUP_TOOLCHAIN', _rust_toolchain(version)),)
|
||||
if version != 'system' else ()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def in_env(prefix: Prefix) -> Generator[None, None, None]:
|
||||
target_dir = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
with envcontext(get_env_patch(target_dir)):
|
||||
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 _add_dependencies(
|
||||
cargo_toml_path: str,
|
||||
additional_dependencies: Set[str],
|
||||
prefix: Prefix,
|
||||
additional_dependencies: set[str],
|
||||
) -> None:
|
||||
with open(cargo_toml_path, 'r+') as f:
|
||||
cargo_toml = toml.load(f)
|
||||
cargo_toml.setdefault('dependencies', {})
|
||||
for dep in additional_dependencies:
|
||||
name, _, spec = dep.partition(':')
|
||||
cargo_toml['dependencies'][name] = spec or '*'
|
||||
f.seek(0)
|
||||
toml.dump(cargo_toml, f)
|
||||
f.truncate()
|
||||
crates = []
|
||||
for dep in additional_dependencies:
|
||||
name, _, spec = dep.partition(':')
|
||||
crate = f'{name}@{spec or "*"}'
|
||||
crates.append(crate)
|
||||
|
||||
lang_base.setup_cmd(prefix, ('cargo', 'add', *crates))
|
||||
|
||||
|
||||
def install_rust_with_toolchain(toolchain: str, envdir: str) -> None:
|
||||
with tempfile.TemporaryDirectory() as rustup_dir:
|
||||
with envcontext((('CARGO_HOME', envdir), ('RUSTUP_HOME', rustup_dir))):
|
||||
# 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(
|
||||
|
|
@ -57,10 +115,7 @@ def install_environment(
|
|||
version: str,
|
||||
additional_dependencies: Sequence[str],
|
||||
) -> None:
|
||||
helpers.assert_version_default('rust', version)
|
||||
directory = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
# There are two cases where we might want to specify more dependencies:
|
||||
# as dependencies for the library being built, and as binary packages
|
||||
|
|
@ -77,30 +132,29 @@ def install_environment(
|
|||
}
|
||||
lib_deps = set(additional_dependencies) - cli_deps
|
||||
|
||||
if len(lib_deps) > 0:
|
||||
_add_dependencies(prefix.path('Cargo.toml'), lib_deps)
|
||||
packages_to_install: set[tuple[str, ...]] = {('--path', '.')}
|
||||
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):
|
||||
packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')}
|
||||
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,))
|
||||
with contextlib.ExitStack() as ctx:
|
||||
ctx.enter_context(in_env(prefix, version))
|
||||
|
||||
if version != 'system':
|
||||
install_rust_with_toolchain(_rust_toolchain(version), envdir)
|
||||
|
||||
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', directory, *args,
|
||||
'cargo', 'install', '--bins', '--root', envdir, *args,
|
||||
cwd=prefix.prefix_dir,
|
||||
)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
with in_env(hook.prefix):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
|
||||
ENVIRONMENT_DIR = None
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
install_environment = helpers.no_install
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:])
|
||||
return helpers.run_xargs(hook, cmd, file_args, color=color)
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from typing import Generator
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
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 PatchesT
|
||||
from pre_commit.envcontext import Var
|
||||
from pre_commit.hook import Hook
|
||||
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
|
||||
|
||||
ENVIRONMENT_DIR = 'swift_env'
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
BUILD_DIR = '.build'
|
||||
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: str) -> PatchesT: # pragma: win32 no cover
|
||||
bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG)
|
||||
|
|
@ -27,10 +27,8 @@ def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover
|
|||
|
||||
|
||||
@contextlib.contextmanager # pragma: win32 no cover
|
||||
def in_env(prefix: Prefix) -> Generator[None, None, None]:
|
||||
envdir = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
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
|
||||
|
||||
|
|
@ -38,27 +36,15 @@ def in_env(prefix: Prefix) -> Generator[None, None, None]:
|
|||
def install_environment(
|
||||
prefix: Prefix, version: str, additional_dependencies: Sequence[str],
|
||||
) -> None: # pragma: win32 no cover
|
||||
helpers.assert_version_default('swift', version)
|
||||
helpers.assert_no_additional_deps('swift', additional_dependencies)
|
||||
directory = prefix.path(
|
||||
helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT),
|
||||
)
|
||||
lang_base.assert_version_default('swift', version)
|
||||
lang_base.assert_no_additional_deps('swift', additional_dependencies)
|
||||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
||||
|
||||
# Build the swift package
|
||||
with clean_path_on_failure(directory):
|
||||
os.mkdir(directory)
|
||||
cmd_output_b(
|
||||
'swift', 'build',
|
||||
'-C', prefix.prefix_dir,
|
||||
'-c', BUILD_CONFIG,
|
||||
'--build-path', os.path.join(directory, BUILD_DIR),
|
||||
)
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]: # pragma: win32 no cover
|
||||
with in_env(hook.prefix):
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
os.mkdir(envdir)
|
||||
cmd_output_b(
|
||||
'swift', 'build',
|
||||
'--package-path', prefix.prefix_dir,
|
||||
'-c', BUILD_CONFIG,
|
||||
'--build-path', os.path.join(envdir, BUILD_DIR),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages import helpers
|
||||
|
||||
|
||||
ENVIRONMENT_DIR = None
|
||||
get_default_version = helpers.basic_get_default_version
|
||||
healthy = helpers.basic_healthy
|
||||
install_environment = helpers.no_install
|
||||
|
||||
|
||||
def run_hook(
|
||||
hook: Hook,
|
||||
file_args: Sequence[str],
|
||||
color: bool,
|
||||
) -> Tuple[int, bytes]:
|
||||
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
|
||||
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,6 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
|
||||
from pre_commit import color
|
||||
from pre_commit import output
|
||||
|
|
@ -30,7 +32,7 @@ class LoggingHandler(logging.Handler):
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def logging_handler(use_color: bool) -> Generator[None, None, None]:
|
||||
def logging_handler(use_color: bool) -> Generator[None]:
|
||||
handler = LoggingHandler(use_color)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Union
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import color
|
||||
from pre_commit import clientlib
|
||||
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.clean import clean
|
||||
from pre_commit.commands.gc import gc
|
||||
|
|
@ -22,11 +23,11 @@ from pre_commit.commands.migrate_config import migrate_config
|
|||
from pre_commit.commands.run import run
|
||||
from pre_commit.commands.sample_config import sample_config
|
||||
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 FatalError
|
||||
from pre_commit.logging_handler import logging_handler
|
||||
from pre_commit.store import Store
|
||||
from pre_commit.util import CalledProcessError
|
||||
|
||||
|
||||
logger = logging.getLogger('pre_commit')
|
||||
|
|
@ -37,17 +38,13 @@ logger = logging.getLogger('pre_commit')
|
|||
# pyvenv
|
||||
os.environ.pop('__PYVENV_LAUNCHER__', None)
|
||||
|
||||
# https://github.com/getsentry/snuba/pull/5388
|
||||
os.environ.pop('PYTHONEXECUTABLE', None)
|
||||
|
||||
COMMANDS_NO_GIT = {'clean', 'gc', 'init-templatedir', 'sample-config'}
|
||||
|
||||
|
||||
def _add_color_option(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
'--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
|
||||
type=color.use_color,
|
||||
metavar='{' + ','.join(color.COLOR_CHOICES) + '}',
|
||||
help='Whether to use color in output. Defaults to `%(default)s`.',
|
||||
)
|
||||
COMMANDS_NO_GIT = {
|
||||
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
|
||||
'validate-config', 'validate-manifest',
|
||||
}
|
||||
|
||||
|
||||
def _add_config_option(parser: argparse.ArgumentParser) -> None:
|
||||
|
|
@ -57,42 +54,19 @@ def _add_config_option(parser: argparse.ArgumentParser) -> None:
|
|||
)
|
||||
|
||||
|
||||
class AppendReplaceDefault(argparse.Action):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.appended = False
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
parser: argparse.ArgumentParser,
|
||||
namespace: argparse.Namespace,
|
||||
values: Union[str, Sequence[str], None],
|
||||
option_string: Optional[str] = None,
|
||||
) -> None:
|
||||
if not self.appended:
|
||||
setattr(namespace, self.dest, [])
|
||||
self.appended = True
|
||||
getattr(namespace, self.dest).append(values)
|
||||
|
||||
|
||||
def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
'-t', '--hook-type', choices=(
|
||||
'pre-commit', 'pre-merge-commit', 'pre-push',
|
||||
'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout',
|
||||
),
|
||||
action=AppendReplaceDefault,
|
||||
default=['pre-commit'],
|
||||
dest='hook_types',
|
||||
'-t', '--hook-type',
|
||||
choices=clientlib.HOOK_TYPES, action='append', dest='hook_types',
|
||||
)
|
||||
|
||||
|
||||
def _add_run_options(parser: argparse.ArgumentParser) -> None:
|
||||
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')
|
||||
mutex_group = parser.add_mutually_exclusive_group(required=False)
|
||||
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.',
|
||||
)
|
||||
mutex_group.add_argument(
|
||||
|
|
@ -104,13 +78,26 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
|
|||
help='When hooks fail, run `git diff` directly afterward.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--hook-stage', choices=C.STAGES, default='commit',
|
||||
'--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 `--from-ref`) -- this option represents the '
|
||||
'(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. '
|
||||
|
|
@ -121,17 +108,42 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
|
|||
parser.add_argument(
|
||||
'--to-ref', '--origin', '-o',
|
||||
help=(
|
||||
'(for usage with `--to-ref`) -- this option represents the '
|
||||
'(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`.',
|
||||
)
|
||||
|
|
@ -144,6 +156,20 @@ def _add_run_options(parser: argparse.ArgumentParser) -> None:
|
|||
'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 _adjust_args_and_chdir(args: argparse.Namespace) -> None:
|
||||
|
|
@ -152,33 +178,28 @@ def _adjust_args_and_chdir(args: argparse.Namespace) -> None:
|
|||
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)
|
||||
|
||||
try:
|
||||
toplevel = git.get_root()
|
||||
except CalledProcessError:
|
||||
raise FatalError(
|
||||
'git failed. Is it installed, and are you in a Git repository '
|
||||
'directory?',
|
||||
)
|
||||
else:
|
||||
if toplevel == '': # pragma: no cover (old git)
|
||||
raise FatalError(
|
||||
'git toplevel unexpectedly empty! make sure you are not '
|
||||
'inside the `.git` directory of your repository.',
|
||||
)
|
||||
else:
|
||||
os.chdir(toplevel)
|
||||
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: Optional[Sequence[str]] = None) -> int:
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
parser = argparse.ArgumentParser(prog='pre-commit')
|
||||
|
||||
|
|
@ -191,16 +212,20 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
|
||||
subparsers = parser.add_subparsers(dest='command')
|
||||
|
||||
autoupdate_parser = subparsers.add_parser(
|
||||
def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser:
|
||||
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(autoupdate_parser)
|
||||
_add_config_option(autoupdate_parser)
|
||||
autoupdate_parser.add_argument(
|
||||
'--bleeding-edge', action='store_true',
|
||||
help=(
|
||||
'Update to the bleeding edge of `master` instead of the latest '
|
||||
'Update to the bleeding edge of `HEAD` instead of the latest '
|
||||
'tagged version (the default behavior).'
|
||||
),
|
||||
)
|
||||
|
|
@ -209,48 +234,43 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
help='Store "frozen" hashes in `rev` instead of tag names',
|
||||
)
|
||||
autoupdate_parser.add_argument(
|
||||
'--repo', dest='repos', action='append', metavar='REPO',
|
||||
'--repo', dest='repos', action='append', metavar='REPO', default=[],
|
||||
help='Only update this repository -- may be specified multiple times.',
|
||||
)
|
||||
|
||||
clean_parser = subparsers.add_parser(
|
||||
'clean', help='Clean out pre-commit files.',
|
||||
autoupdate_parser.add_argument(
|
||||
'-j', '--jobs', type=int, default=1,
|
||||
help='Number of threads to use. (default %(default)s).',
|
||||
)
|
||||
_add_color_option(clean_parser)
|
||||
_add_config_option(clean_parser)
|
||||
|
||||
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',
|
||||
_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`.',
|
||||
)
|
||||
hook_impl_parser.add_argument(dest='rest', nargs=argparse.REMAINDER)
|
||||
hazmat.add_parsers(hazmat_parser)
|
||||
|
||||
gc_parser = subparsers.add_parser('gc', help='Clean unused cached repos.')
|
||||
_add_color_option(gc_parser)
|
||||
_add_config_option(gc_parser)
|
||||
|
||||
init_templatedir_parser = subparsers.add_parser(
|
||||
init_templatedir_parser = _add_cmd(
|
||||
'init-templatedir',
|
||||
help=(
|
||||
'Install hook script in a directory intended for use with '
|
||||
'`git config init.templateDir`.'
|
||||
),
|
||||
)
|
||||
_add_color_option(init_templatedir_parser)
|
||||
_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 = subparsers.add_parser(
|
||||
'install', help='Install the pre-commit script.',
|
||||
)
|
||||
_add_color_option(install_parser)
|
||||
install_parser = _add_cmd('install', help='Install the pre-commit script.')
|
||||
_add_config_option(install_parser)
|
||||
install_parser.add_argument(
|
||||
'-f', '--overwrite', action='store_true',
|
||||
|
|
@ -265,14 +285,14 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
)
|
||||
_add_hook_type_option(install_parser)
|
||||
install_parser.add_argument(
|
||||
'--allow-missing-config', action='store_true', default=False,
|
||||
'--allow-missing-config', action='store_true',
|
||||
help=(
|
||||
'Whether to allow a missing `pre-commit` configuration file '
|
||||
'or exit with a failure code.'
|
||||
),
|
||||
)
|
||||
|
||||
install_hooks_parser = subparsers.add_parser(
|
||||
install_hooks_parser = _add_cmd(
|
||||
'install-hooks',
|
||||
help=(
|
||||
'Install hook environments for all environments in the config '
|
||||
|
|
@ -280,32 +300,24 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
'useful.'
|
||||
),
|
||||
)
|
||||
_add_color_option(install_hooks_parser)
|
||||
_add_config_option(install_hooks_parser)
|
||||
|
||||
migrate_config_parser = subparsers.add_parser(
|
||||
migrate_config_parser = _add_cmd(
|
||||
'migrate-config',
|
||||
help='Migrate list configuration to new map configuration.',
|
||||
)
|
||||
_add_color_option(migrate_config_parser)
|
||||
_add_config_option(migrate_config_parser)
|
||||
|
||||
run_parser = subparsers.add_parser('run', help='Run hooks.')
|
||||
_add_color_option(run_parser)
|
||||
run_parser = _add_cmd('run', help='Run hooks.')
|
||||
_add_config_option(run_parser)
|
||||
_add_run_options(run_parser)
|
||||
|
||||
sample_config_parser = subparsers.add_parser(
|
||||
'sample-config', help=f'Produce a sample {C.CONFIG_FILE} file',
|
||||
)
|
||||
_add_color_option(sample_config_parser)
|
||||
_add_config_option(sample_config_parser)
|
||||
_add_cmd('sample-config', help=f'Produce a sample {C.CONFIG_FILE} file')
|
||||
|
||||
try_repo_parser = subparsers.add_parser(
|
||||
try_repo_parser = _add_cmd(
|
||||
'try-repo',
|
||||
help='Try the hooks in a repository, useful for developing new hooks.',
|
||||
)
|
||||
_add_color_option(try_repo_parser)
|
||||
_add_config_option(try_repo_parser)
|
||||
try_repo_parser.add_argument(
|
||||
'repo', help='Repository to source hooks from.',
|
||||
|
|
@ -319,18 +331,39 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
)
|
||||
_add_run_options(try_repo_parser)
|
||||
|
||||
uninstall_parser = subparsers.add_parser(
|
||||
uninstall_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)
|
||||
|
||||
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', help='Show help for a specific command.',
|
||||
)
|
||||
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
|
||||
if len(argv) == 0:
|
||||
argv = ['run']
|
||||
|
|
@ -342,25 +375,28 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
parser.parse_args(['--help'])
|
||||
|
||||
with error_handler(), logging_handler(args.color):
|
||||
if args.command not in COMMANDS_NO_GIT:
|
||||
_adjust_args_and_chdir(args)
|
||||
|
||||
git.check_for_cygwin_mismatch()
|
||||
|
||||
store = Store()
|
||||
store.mark_config_used(args.config)
|
||||
|
||||
if args.command not in COMMANDS_NO_GIT:
|
||||
_adjust_args_and_chdir(args)
|
||||
store.mark_config_used(args.config)
|
||||
|
||||
if args.command == 'autoupdate':
|
||||
return autoupdate(
|
||||
args.config, store,
|
||||
args.config,
|
||||
tags_only=not args.bleeding_edge,
|
||||
freeze=args.freeze,
|
||||
repos=args.repos,
|
||||
jobs=args.jobs,
|
||||
)
|
||||
elif args.command == 'clean':
|
||||
return clean(store)
|
||||
elif args.command == 'gc':
|
||||
return gc(store)
|
||||
elif args.command == 'hazmat':
|
||||
return hazmat.impl(args)
|
||||
elif args.command == 'hook-impl':
|
||||
return hook_impl(
|
||||
store,
|
||||
|
|
@ -383,6 +419,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
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)
|
||||
|
|
@ -395,7 +432,14 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
elif args.command == 'try-repo':
|
||||
return try_repo(args)
|
||||
elif args.command == 'uninstall':
|
||||
return uninstall(hook_types=args.hook_types)
|
||||
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:
|
||||
raise NotImplementedError(
|
||||
f'Command {args.command} not implemented.',
|
||||
|
|
@ -407,4 +451,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
import argparse
|
||||
import os.path
|
||||
import tarfile
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
from pre_commit import output
|
||||
from pre_commit.util import cmd_output_b
|
||||
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', 'a3fa9b7'),
|
||||
('ruby-build', 'git://github.com/rbenv/ruby-build', '1a902f3'),
|
||||
(
|
||||
'ruby-download',
|
||||
'git://github.com/garnieretienne/rvm-download',
|
||||
'09bd7c6',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def make_archive(name: str, repo: str, ref: str, destdir: str) -> str:
|
||||
"""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, f'{name}.tar.gz')
|
||||
with tmpdir() as tempdir:
|
||||
# Clone the repository to the temporary directory
|
||||
cmd_output_b('git', 'clone', repo, tempdir)
|
||||
cmd_output_b('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: Optional[Sequence[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--dest', default='pre_commit/resources')
|
||||
args = parser.parse_args(argv)
|
||||
for archive_name, repo, ref in REPOS:
|
||||
output.write_line(f'Making {archive_name}.tar.gz for {repo}@{ref}')
|
||||
make_archive(archive_name, repo, ref, args.dest)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import git
|
||||
|
|
@ -20,14 +21,14 @@ def check_all_hooks_match_files(config_file: str) -> int:
|
|||
for hook in all_hooks(config, Store()):
|
||||
if hook.always_run or hook.language == 'fail':
|
||||
continue
|
||||
elif not classifier.filenames_for_hook(hook):
|
||||
elif not any(classifier.filenames_for_hook(hook)):
|
||||
print(f'{hook.id} does not apply to this repository')
|
||||
retv = 1
|
||||
|
||||
return retv
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
||||
args = parser.parse_args(argv)
|
||||
|
|
@ -39,4 +40,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Sequence
|
||||
|
||||
from cfgv import apply_defaults
|
||||
|
||||
|
|
@ -13,7 +15,7 @@ from pre_commit.commands.run import Classifier
|
|||
|
||||
|
||||
def exclude_matches_any(
|
||||
filenames: Sequence[str],
|
||||
filenames: Iterable[str],
|
||||
include: str,
|
||||
exclude: str,
|
||||
) -> bool:
|
||||
|
|
@ -43,12 +45,18 @@ def check_useless_excludes(config_file: str) -> int:
|
|||
|
||||
for repo in config['repos']:
|
||||
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
|
||||
# the defaults applied during runtime
|
||||
hook = apply_defaults(hook, MANIFEST_HOOK_DICT)
|
||||
names = classifier.filenames
|
||||
types, exclude_types = hook['types'], hook['exclude_types']
|
||||
names = classifier.by_types(names, types, exclude_types)
|
||||
names = classifier.by_types(
|
||||
classifier.filenames,
|
||||
hook['types'],
|
||||
hook['types_or'],
|
||||
hook['exclude_types'],
|
||||
)
|
||||
include, exclude = hook['files'], hook['exclude']
|
||||
if not exclude_matches_any(names, include, exclude):
|
||||
print(
|
||||
|
|
@ -60,7 +68,7 @@ def check_useless_excludes(config_file: str) -> int:
|
|||
return retv
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
|
||||
args = parser.parse_args(argv)
|
||||
|
|
@ -72,4 +80,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pre_commit import output
|
||||
|
||||
|
||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
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)
|
||||
|
|
@ -13,4 +14,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import IO
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
|
||||
|
|
@ -11,9 +12,9 @@ def write(s: str, stream: IO[bytes] = sys.stdout.buffer) -> None:
|
|||
|
||||
|
||||
def write_line_b(
|
||||
s: Optional[bytes] = None,
|
||||
s: bytes | None = None,
|
||||
stream: IO[bytes] = sys.stdout.buffer,
|
||||
logfile_name: Optional[str] = None,
|
||||
logfile_name: str | None = None,
|
||||
) -> None:
|
||||
with contextlib.ExitStack() as exit_stack:
|
||||
output_streams = [stream]
|
||||
|
|
@ -28,5 +29,5 @@ def write_line_b(
|
|||
output_stream.flush()
|
||||
|
||||
|
||||
def write_line(s: Optional[str] = None, **kwargs: Any) -> None:
|
||||
def write_line(s: str | None = None, **kwargs: Any) -> None:
|
||||
write_line_b(s.encode() if s is not None else s, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from collections.abc import Mapping
|
||||
from typing import NoReturn
|
||||
|
||||
from identify.identify import parse_shebang_from_file
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
class ExecutableNotFoundError(OSError):
|
||||
def to_output(self) -> Tuple[int, bytes, None]:
|
||||
def to_output(self) -> tuple[int, bytes, None]:
|
||||
return (1, self.args[0].encode(), None)
|
||||
|
||||
|
||||
def parse_filename(filename: str) -> Tuple[str, ...]:
|
||||
def parse_filename(filename: str) -> tuple[str, ...]:
|
||||
if not os.path.exists(filename):
|
||||
return ()
|
||||
else:
|
||||
|
|
@ -23,13 +20,13 @@ def parse_filename(filename: str) -> Tuple[str, ...]:
|
|||
|
||||
|
||||
def find_executable(
|
||||
exe: str, _environ: Optional[Mapping[str, str]] = None,
|
||||
) -> Optional[str]:
|
||||
exe: str, *, env: Mapping[str, str] | None = None,
|
||||
) -> str | None:
|
||||
exe = os.path.normpath(exe)
|
||||
if os.sep in 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:
|
||||
exts = environ['PATHEXT'].split(os.pathsep)
|
||||
|
|
@ -46,12 +43,12 @@ def find_executable(
|
|||
return None
|
||||
|
||||
|
||||
def normexe(orig: str) -> str:
|
||||
def _error(msg: str) -> 'NoReturn':
|
||||
def normexe(orig: str, *, env: Mapping[str, str] | None = None) -> str:
|
||||
def _error(msg: str) -> NoReturn:
|
||||
raise ExecutableNotFoundError(f'Executable `{orig}` {msg}')
|
||||
|
||||
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:
|
||||
_error('not found')
|
||||
return exe
|
||||
|
|
@ -65,7 +62,11 @@ def normexe(orig: str) -> str:
|
|||
return orig
|
||||
|
||||
|
||||
def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]:
|
||||
def normalize_cmd(
|
||||
cmd: tuple[str, ...],
|
||||
*,
|
||||
env: Mapping[str, str] | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Fixes for the following issues on windows
|
||||
- https://bugs.python.org/issue8557
|
||||
- windows does not parse shebangs
|
||||
|
|
@ -73,12 +74,12 @@ def normalize_cmd(cmd: Tuple[str, ...]) -> Tuple[str, ...]:
|
|||
This function also makes deep-path shebangs work just fine
|
||||
"""
|
||||
# Use PATH to determine the executable
|
||||
exe = normexe(cmd[0])
|
||||
exe = normexe(cmd[0], env=env)
|
||||
|
||||
# Figure out the shebang from the resulting command
|
||||
cmd = parse_filename(exe) + (exe,) + cmd[1:]
|
||||
|
||||
# This could have given us back another bare executable
|
||||
exe = normexe(cmd[0])
|
||||
exe = normexe(cmd[0], env=env)
|
||||
|
||||
return (exe,) + cmd[1:]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from typing import NamedTuple
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class Prefix(NamedTuple):
|
||||
|
|
@ -12,6 +13,6 @@ class Prefix(NamedTuple):
|
|||
def exists(self, *parts: str) -> bool:
|
||||
return os.path.exists(self.path(*parts))
|
||||
|
||||
def star(self, end: str) -> Tuple[str, ...]:
|
||||
def star(self, end: str) -> tuple[str, ...]:
|
||||
paths = os.listdir(self.prefix_dir)
|
||||
return tuple(path for path in paths if path.endswith(end))
|
||||
|
|
|
|||
|
|
@ -1,40 +1,41 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit.all_languages import languages
|
||||
from pre_commit.clientlib import load_manifest
|
||||
from pre_commit.clientlib import LOCAL
|
||||
from pre_commit.clientlib import META
|
||||
from pre_commit.hook import Hook
|
||||
from pre_commit.languages.all import languages
|
||||
from pre_commit.languages.helpers import environment_dir
|
||||
from pre_commit.lang_base import environment_dir
|
||||
from pre_commit.prefix import Prefix
|
||||
from pre_commit.store import Store
|
||||
from pre_commit.util import parse_version
|
||||
from pre_commit.util import clean_path_on_failure
|
||||
from pre_commit.util import rmtree
|
||||
|
||||
|
||||
logger = logging.getLogger('pre_commit')
|
||||
|
||||
|
||||
def _state_filename_v1(venv: str) -> str:
|
||||
return os.path.join(venv, '.install_state_v1')
|
||||
|
||||
|
||||
def _state_filename_v2(venv: str) -> str:
|
||||
return os.path.join(venv, '.install_state_v2')
|
||||
|
||||
|
||||
def _state(additional_deps: Sequence[str]) -> object:
|
||||
return {'additional_dependencies': sorted(additional_deps)}
|
||||
return {'additional_dependencies': additional_deps}
|
||||
|
||||
|
||||
def _state_filename(prefix: Prefix, venv: str) -> str:
|
||||
return prefix.path(venv, f'.install_state_v{C.INSTALLED_STATE_VERSION}')
|
||||
|
||||
|
||||
def _read_state(prefix: Prefix, venv: str) -> Optional[object]:
|
||||
filename = _state_filename(prefix, venv)
|
||||
def _read_state(venv: str) -> object | None:
|
||||
filename = _state_filename_v1(venv)
|
||||
if not os.path.exists(filename):
|
||||
return None
|
||||
else:
|
||||
|
|
@ -42,26 +43,22 @@ def _read_state(prefix: Prefix, venv: str) -> Optional[object]:
|
|||
return json.load(f)
|
||||
|
||||
|
||||
def _write_state(prefix: Prefix, venv: str, state: object) -> None:
|
||||
state_filename = _state_filename(prefix, venv)
|
||||
staging = f'{state_filename}staging'
|
||||
with open(staging, 'w') as state_file:
|
||||
state_file.write(json.dumps(state))
|
||||
# Move the file into place atomically to indicate we've installed
|
||||
os.rename(staging, state_filename)
|
||||
|
||||
|
||||
def _hook_installed(hook: Hook) -> bool:
|
||||
lang = languages[hook.language]
|
||||
venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version)
|
||||
if lang.ENVIRONMENT_DIR is None:
|
||||
return True
|
||||
|
||||
venv = environment_dir(
|
||||
hook.prefix,
|
||||
lang.ENVIRONMENT_DIR,
|
||||
hook.language_version,
|
||||
)
|
||||
return (
|
||||
venv is None or (
|
||||
(
|
||||
_read_state(hook.prefix, venv) ==
|
||||
_state(hook.additional_dependencies)
|
||||
) and
|
||||
lang.healthy(hook.prefix, hook.language_version)
|
||||
)
|
||||
(
|
||||
os.path.exists(_state_filename_v2(venv)) or
|
||||
_read_state(venv) == _state(hook.additional_dependencies)
|
||||
) and
|
||||
not lang.health_check(hook.prefix, hook.language_version)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -72,37 +69,51 @@ def _hook_install(hook: Hook) -> None:
|
|||
|
||||
lang = languages[hook.language]
|
||||
assert lang.ENVIRONMENT_DIR is not None
|
||||
venv = environment_dir(lang.ENVIRONMENT_DIR, hook.language_version)
|
||||
|
||||
venv = environment_dir(
|
||||
hook.prefix,
|
||||
lang.ENVIRONMENT_DIR,
|
||||
hook.language_version,
|
||||
)
|
||||
|
||||
# There's potentially incomplete cleanup from previous runs
|
||||
# Clean it up!
|
||||
if hook.prefix.exists(venv):
|
||||
rmtree(hook.prefix.path(venv))
|
||||
if os.path.exists(venv):
|
||||
rmtree(venv)
|
||||
|
||||
lang.install_environment(
|
||||
hook.prefix, hook.language_version, hook.additional_dependencies,
|
||||
)
|
||||
# Write our state to indicate we're installed
|
||||
_write_state(hook.prefix, venv, _state(hook.additional_dependencies))
|
||||
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]:
|
||||
*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)
|
||||
|
||||
version = ret['minimum_pre_commit_version']
|
||||
if parse_version(version) > parse_version(C.VERSION):
|
||||
logger.error(
|
||||
f'The hook `{ret["id"]}` requires pre-commit version {version} '
|
||||
f'but version {C.VERSION} is installed. '
|
||||
f'Perhaps run `pip install --upgrade pre-commit`.',
|
||||
)
|
||||
exit(1)
|
||||
|
||||
lang = ret['language']
|
||||
if ret['language_version'] == C.DEFAULT:
|
||||
ret['language_version'] = root_config['default_language_version'][lang]
|
||||
|
|
@ -112,14 +123,32 @@ def _hook(
|
|||
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],
|
||||
repo_config: dict[str, Any],
|
||||
store: Store,
|
||||
root_config: Dict[str, Any],
|
||||
) -> Tuple[Hook, ...]:
|
||||
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
|
||||
|
|
@ -140,10 +169,10 @@ def _non_cloned_repository_hooks(
|
|||
|
||||
|
||||
def _cloned_repository_hooks(
|
||||
repo_config: Dict[str, Any],
|
||||
repo_config: dict[str, Any],
|
||||
store: Store,
|
||||
root_config: Dict[str, Any],
|
||||
) -> Tuple[Hook, ...]:
|
||||
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)}
|
||||
|
|
@ -172,10 +201,10 @@ def _cloned_repository_hooks(
|
|||
|
||||
|
||||
def _repository_hooks(
|
||||
repo_config: Dict[str, Any],
|
||||
repo_config: dict[str, Any],
|
||||
store: Store,
|
||||
root_config: Dict[str, Any],
|
||||
) -> Tuple[Hook, ...]:
|
||||
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:
|
||||
|
|
@ -183,8 +212,8 @@ def _repository_hooks(
|
|||
|
||||
|
||||
def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
|
||||
def _need_installed() -> List[Hook]:
|
||||
seen: Set[Tuple[Prefix, str, str, Tuple[str, ...]]] = set()
|
||||
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):
|
||||
|
|
@ -200,7 +229,7 @@ def install_hook_envs(hooks: Sequence[Hook], store: Store) -> None:
|
|||
_hook_install(hook)
|
||||
|
||||
|
||||
def all_hooks(root_config: Dict[str, Any], store: Store) -> Tuple[Hook, ...]:
|
||||
def all_hooks(root_config: dict[str, Any], store: Store) -> tuple[Hook, ...]:
|
||||
return tuple(
|
||||
hook
|
||||
for repo in root_config['repos']
|
||||
|
|
|
|||
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.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use ExtUtils::MakeMaker;
|
||||
|
||||
WriteMakefile(
|
||||
NAME => "PreCommitDummy",
|
||||
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)
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
module pre-commit-placeholder-empty-module
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"name": "pre_commit_dummy_package",
|
||||
"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 = {},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from setuptools import setup
|
||||
|
||||
|
||||
setup(name='pre-commit-dummy-package', version='0.0.0')
|
||||
setup(name='pre-commit-placeholder-package', version='0.0.0', py_modules=[])
|
||||
|
|
|
|||
|
|
@ -1,44 +1,20 @@
|
|||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env bash
|
||||
# File generated by pre-commit: https://pre-commit.com
|
||||
# ID: 138fd403232d2ddd5efb44317e38bf03
|
||||
import os
|
||||
import sys
|
||||
|
||||
# we try our best, but the shebang of this script is difficult to determine:
|
||||
# - macos doesn't ship with python3
|
||||
# - windows executables are almost always `python.exe`
|
||||
# therefore we continue to support python2 for this small script
|
||||
if sys.version_info < (3, 3):
|
||||
from distutils.spawn import find_executable as which
|
||||
else:
|
||||
from shutil import which
|
||||
|
||||
# work around https://github.com/Homebrew/homebrew-core/issues/30445
|
||||
os.environ.pop('__PYVENV_LAUNCHER__', None)
|
||||
|
||||
# start templated
|
||||
INSTALL_PYTHON = ''
|
||||
ARGS = ['hook-impl']
|
||||
INSTALL_PYTHON=''
|
||||
ARGS=(hook-impl)
|
||||
# end templated
|
||||
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
|
||||
ARGS.append('--')
|
||||
ARGS.extend(sys.argv[1:])
|
||||
|
||||
DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?'
|
||||
if os.access(INSTALL_PYTHON, os.X_OK):
|
||||
CMD = [INSTALL_PYTHON, '-mpre_commit']
|
||||
elif which('pre-commit'):
|
||||
CMD = ['pre-commit']
|
||||
else:
|
||||
raise SystemExit(DNE)
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ARGS+=(--hook-dir "$HERE" -- "$@")
|
||||
|
||||
CMD.extend(ARGS)
|
||||
if sys.platform == 'win32': # https://bugs.python.org/issue19124
|
||||
import subprocess
|
||||
|
||||
if sys.version_info < (3, 7): # https://bugs.python.org/issue25942
|
||||
raise SystemExit(subprocess.Popen(CMD).wait())
|
||||
else:
|
||||
raise SystemExit(subprocess.call(CMD))
|
||||
else:
|
||||
os.execvp(CMD[0], CMD)
|
||||
if [ -x "$INSTALL_PYTHON" ]; then
|
||||
exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
|
||||
elif command -v pre-commit > /dev/null; then
|
||||
exec pre-commit "${ARGS[@]}"
|
||||
else
|
||||
echo '`pre-commit` not found. Did you forget to activate your virtualenv?' 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,10 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os.path
|
||||
import time
|
||||
from typing import Generator
|
||||
from collections.abc import Generator
|
||||
|
||||
from pre_commit import git
|
||||
from pre_commit.errors import FatalError
|
||||
from pre_commit.util import CalledProcessError
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import cmd_output_b
|
||||
|
|
@ -13,6 +16,12 @@ from pre_commit.xargs import xargs
|
|||
|
||||
logger = logging.getLogger('pre_commit')
|
||||
|
||||
# without forcing submodule.recurse=0, changes in nested submodules will be
|
||||
# discarded if `submodule.recurse=1` is configured
|
||||
# we choose this instead of `--no-recurse-submodules` because it works on
|
||||
# versions of git before that option was added to `git checkout`
|
||||
_CHECKOUT_CMD = ('git', '-c', 'submodule.recurse=0', 'checkout', '--', '.')
|
||||
|
||||
|
||||
def _git_apply(patch: str) -> None:
|
||||
args = ('apply', '--whitespace=nowarn', patch)
|
||||
|
|
@ -24,7 +33,7 @@ def _git_apply(patch: str) -> None:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _intent_to_add_cleared() -> Generator[None, None, None]:
|
||||
def _intent_to_add_cleared() -> Generator[None]:
|
||||
intent_to_add = git.intent_to_add_files()
|
||||
if intent_to_add:
|
||||
logger.warning('Unstaged intent-to-add files detected.')
|
||||
|
|
@ -39,28 +48,37 @@ def _intent_to_add_cleared() -> Generator[None, None, None]:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
|
||||
def _unstaged_changes_cleared(patch_dir: str) -> Generator[None]:
|
||||
tree = cmd_output('git', 'write-tree')[1].strip()
|
||||
retcode, diff_stdout_binary, _ = cmd_output_b(
|
||||
diff_cmd = (
|
||||
'git', 'diff-index', '--ignore-submodules', '--binary',
|
||||
'--exit-code', '--no-color', '--no-ext-diff', tree, '--',
|
||||
retcode=None,
|
||||
)
|
||||
if retcode and diff_stdout_binary.strip():
|
||||
patch_filename = f'patch{int(time.time())}'
|
||||
retcode, diff_stdout, diff_stderr = cmd_output_b(*diff_cmd, check=False)
|
||||
if retcode == 0:
|
||||
# There weren't any staged files so we don't need to do anything
|
||||
# special
|
||||
yield
|
||||
elif retcode == 1 and not diff_stdout.strip():
|
||||
# due to behaviour (probably a bug?) in git with crlf endings and
|
||||
# autocrlf set to either `true` or `input` sometimes git will refuse
|
||||
# to show a crlf-only diff to us :(
|
||||
yield
|
||||
elif retcode == 1 and diff_stdout.strip():
|
||||
patch_filename = f'patch{int(time.time())}-{os.getpid()}'
|
||||
patch_filename = os.path.join(patch_dir, patch_filename)
|
||||
logger.warning('Unstaged files detected.')
|
||||
logger.info(f'Stashing unstaged files to {patch_filename}.')
|
||||
# Save the current unstaged changes as a patch
|
||||
os.makedirs(patch_dir, exist_ok=True)
|
||||
with open(patch_filename, 'wb') as patch_file:
|
||||
patch_file.write(diff_stdout_binary)
|
||||
patch_file.write(diff_stdout)
|
||||
|
||||
# prevent recursive post-checkout hooks (#1418)
|
||||
no_checkout_env = dict(os.environ, _PRE_COMMIT_SKIP_POST_CHECKOUT='1')
|
||||
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
|
||||
|
||||
try:
|
||||
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
|
||||
yield
|
||||
finally:
|
||||
# Try to apply the patch we saved
|
||||
|
|
@ -74,18 +92,20 @@ def _unstaged_changes_cleared(patch_dir: str) -> Generator[None, None, None]:
|
|||
# We failed to apply the patch, presumably due to fixes made
|
||||
# by hooks.
|
||||
# Roll back the changes made by hooks.
|
||||
cmd_output_b('git', 'checkout', '--', '.', env=no_checkout_env)
|
||||
cmd_output_b(*_CHECKOUT_CMD, env=no_checkout_env)
|
||||
_git_apply(patch_filename)
|
||||
|
||||
logger.info(f'Restored changes from {patch_filename}.')
|
||||
else:
|
||||
# There weren't any staged files so we don't need to do anything
|
||||
# special
|
||||
yield
|
||||
else: # pragma: win32 no cover
|
||||
# some error occurred while requesting the diff
|
||||
e = CalledProcessError(retcode, diff_cmd, b'', diff_stderr)
|
||||
raise FatalError(
|
||||
f'pre-commit failed to diff -- perhaps due to permissions?\n\n{e}',
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def staged_files_only(patch_dir: str) -> Generator[None, None, None]:
|
||||
def staged_files_only(patch_dir: str) -> Generator[None]:
|
||||
"""Clear any unstaged changes from the git working directory inside this
|
||||
context.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os.path
|
||||
import sqlite3
|
||||
import tempfile
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Sequence
|
||||
|
||||
import pre_commit.constants as C
|
||||
from pre_commit import clientlib
|
||||
from pre_commit import file_lock
|
||||
from pre_commit import git
|
||||
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 resource_text
|
||||
from pre_commit.util import rmtree
|
||||
|
||||
|
||||
logger = logging.getLogger('pre_commit')
|
||||
|
|
@ -37,12 +36,36 @@ def _get_default_directory() -> str:
|
|||
return os.path.realpath(ret)
|
||||
|
||||
|
||||
_LOCAL_RESOURCES = (
|
||||
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
|
||||
'package.json', 'pre-commit-package-dev-1.rockspec',
|
||||
'pre_commit_placeholder_package.gemspec', 'setup.py',
|
||||
'environment.yml', 'Makefile.PL', 'pubspec.yaml',
|
||||
'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv',
|
||||
)
|
||||
|
||||
|
||||
def _make_local_repo(directory: str) -> None:
|
||||
for resource in _LOCAL_RESOURCES:
|
||||
resource_dirname, resource_basename = os.path.split(resource)
|
||||
contents = resource_text(f'empty_template_{resource_basename}')
|
||||
target_dir = os.path.join(directory, resource_dirname)
|
||||
target_file = os.path.join(target_dir, resource_basename)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
with open(target_file, 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
class Store:
|
||||
get_default_directory = staticmethod(_get_default_directory)
|
||||
|
||||
def __init__(self, directory: Optional[str] = None) -> None:
|
||||
def __init__(self, directory: str | None = None) -> None:
|
||||
self.directory = directory or Store.get_default_directory()
|
||||
self.db_path = os.path.join(self.directory, 'db.db')
|
||||
self.readonly = (
|
||||
os.path.exists(self.directory) and
|
||||
not os.access(self.directory, os.W_OK)
|
||||
)
|
||||
|
||||
if not os.path.exists(self.directory):
|
||||
os.makedirs(self.directory, exist_ok=True)
|
||||
|
|
@ -72,13 +95,13 @@ class Store:
|
|||
' PRIMARY KEY (repo, ref)'
|
||||
');',
|
||||
)
|
||||
self._create_config_table(db)
|
||||
self._create_configs_table(db)
|
||||
|
||||
# Atomic file move
|
||||
os.rename(tmpfile, self.db_path)
|
||||
os.replace(tmpfile, self.db_path)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def exclusive_lock(self) -> Generator[None, None, None]:
|
||||
def exclusive_lock(self) -> Generator[None]:
|
||||
def blocked_cb() -> None: # pragma: no cover (tests are in-process)
|
||||
logger.info('Locking pre-commit directory')
|
||||
|
||||
|
|
@ -88,8 +111,8 @@ class Store:
|
|||
@contextlib.contextmanager
|
||||
def connect(
|
||||
self,
|
||||
db_path: Optional[str] = None,
|
||||
) -> Generator[sqlite3.Connection, None, None]:
|
||||
db_path: str | None = None,
|
||||
) -> Generator[sqlite3.Connection]:
|
||||
db_path = db_path or self.db_path
|
||||
# sqlite doesn't close its fd with its contextmanager >.<
|
||||
# contextlib.closing fixes this.
|
||||
|
|
@ -102,7 +125,7 @@ class Store:
|
|||
@classmethod
|
||||
def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:
|
||||
if deps:
|
||||
return f'{repo}:{",".join(sorted(deps))}'
|
||||
return f'{repo}:{",".join(deps)}'
|
||||
else:
|
||||
return repo
|
||||
|
||||
|
|
@ -113,9 +136,10 @@ class Store:
|
|||
deps: Sequence[str],
|
||||
make_strategy: Callable[[str], None],
|
||||
) -> str:
|
||||
original_repo = repo
|
||||
repo = self.db_repo_name(repo, deps)
|
||||
|
||||
def _get_result() -> Optional[str]:
|
||||
def _get_result() -> str | None:
|
||||
# Check if we already exist
|
||||
with self.connect() as db:
|
||||
result = db.execute(
|
||||
|
|
@ -145,6 +169,9 @@ class Store:
|
|||
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
|
||||
[repo, ref, directory],
|
||||
)
|
||||
|
||||
clientlib.warn_for_stages_on_repo_init(original_repo, directory)
|
||||
|
||||
return directory
|
||||
|
||||
def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
|
||||
|
|
@ -182,34 +209,12 @@ class Store:
|
|||
|
||||
return self._new_repo(repo, ref, deps, clone_strategy)
|
||||
|
||||
LOCAL_RESOURCES = (
|
||||
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
|
||||
'package.json', 'pre_commit_dummy_package.gemspec', 'setup.py',
|
||||
'environment.yml', 'Makefile.PL',
|
||||
)
|
||||
|
||||
def make_local(self, deps: Sequence[str]) -> str:
|
||||
def make_local_strategy(directory: str) -> None:
|
||||
for resource in self.LOCAL_RESOURCES:
|
||||
contents = resource_text(f'empty_template_{resource}')
|
||||
with open(os.path.join(directory, resource), 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
env = git.no_git_env()
|
||||
|
||||
# initialize the git repository so it looks more like cloned repos
|
||||
def _git_cmd(*args: str) -> None:
|
||||
cmd_output_b('git', *args, cwd=directory, env=env)
|
||||
|
||||
git.init_repo(directory, '<<unknown>>')
|
||||
_git_cmd('add', '.')
|
||||
git.commit(repo=directory)
|
||||
|
||||
return self._new_repo(
|
||||
'local', C.LOCAL_REPO_VERSION, deps, make_local_strategy,
|
||||
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
|
||||
)
|
||||
|
||||
def _create_config_table(self, db: sqlite3.Connection) -> None:
|
||||
def _create_configs_table(self, db: sqlite3.Connection) -> None:
|
||||
db.executescript(
|
||||
'CREATE TABLE IF NOT EXISTS configs ('
|
||||
' path TEXT NOT NULL,'
|
||||
|
|
@ -218,34 +223,13 @@ class Store:
|
|||
)
|
||||
|
||||
def mark_config_used(self, path: str) -> None:
|
||||
if self.readonly: # pragma: win32 no cover
|
||||
return
|
||||
path = os.path.realpath(path)
|
||||
# don't insert config files that do not exist
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
with self.connect() as db:
|
||||
# TODO: eventually remove this and only create in _create
|
||||
self._create_config_table(db)
|
||||
self._create_configs_table(db)
|
||||
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
|
||||
|
||||
def select_all_configs(self) -> List[str]:
|
||||
with self.connect() as db:
|
||||
self._create_config_table(db)
|
||||
rows = db.execute('SELECT path FROM configs').fetchall()
|
||||
return [path for path, in rows]
|
||||
|
||||
def delete_configs(self, configs: List[str]) -> None:
|
||||
with self.connect() as db:
|
||||
rows = [(path,) for path in configs]
|
||||
db.executemany('DELETE FROM configs WHERE path = ?', rows)
|
||||
|
||||
def select_all_repos(self) -> List[Tuple[str, str, str]]:
|
||||
with self.connect() as db:
|
||||
return db.execute('SELECT repo, ref, path from repos').fetchall()
|
||||
|
||||
def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
|
||||
with self.connect() as db:
|
||||
db.execute(
|
||||
'DELETE FROM repos WHERE repo = ? and ref = ?',
|
||||
(db_repo_name, ref),
|
||||
)
|
||||
rmtree(path)
|
||||
|
|
|
|||
|
|
@ -1,48 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import functools
|
||||
import importlib.resources
|
||||
import os.path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Generator
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import IO
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
import yaml
|
||||
|
||||
from pre_commit import parse_shebang
|
||||
|
||||
if sys.version_info >= (3, 7): # pragma: no cover (PY37+)
|
||||
from importlib.resources import open_binary
|
||||
from importlib.resources import read_text
|
||||
else: # pragma: no cover (<PY37)
|
||||
from importlib_resources import open_binary
|
||||
from importlib_resources import read_text
|
||||
|
||||
EnvironT = Union[Dict[str, str], 'os._Environ']
|
||||
|
||||
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
|
||||
yaml_load = functools.partial(yaml.load, Loader=Loader)
|
||||
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
|
||||
|
||||
|
||||
def yaml_dump(o: Any, **kwargs: Any) -> str:
|
||||
# when python/mypy#1484 is solved, this can be `functools.partial`
|
||||
return yaml.dump(
|
||||
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def force_bytes(exc: Any) -> bytes:
|
||||
with contextlib.suppress(TypeError):
|
||||
|
|
@ -53,7 +25,7 @@ def force_bytes(exc: Any) -> bytes:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def clean_path_on_failure(path: str) -> Generator[None, None, None]:
|
||||
def clean_path_on_failure(path: str) -> Generator[None]:
|
||||
"""Cleans up the directory on an exceptional failure."""
|
||||
try:
|
||||
yield
|
||||
|
|
@ -63,24 +35,9 @@ def clean_path_on_failure(path: str) -> Generator[None, None, None]:
|
|||
raise
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tmpdir() -> Generator[str, None, None]:
|
||||
"""Contextmanager to create a temporary directory. It will be cleaned up
|
||||
afterwards.
|
||||
"""
|
||||
tempdir = tempfile.mkdtemp()
|
||||
try:
|
||||
yield tempdir
|
||||
finally:
|
||||
rmtree(tempdir)
|
||||
|
||||
|
||||
def resource_bytesio(filename: str) -> IO[bytes]:
|
||||
return open_binary('pre_commit.resources', filename)
|
||||
|
||||
|
||||
def resource_text(filename: str) -> str:
|
||||
return read_text('pre_commit.resources', filename)
|
||||
files = importlib.resources.files('pre_commit.resources')
|
||||
return files.joinpath(filename).read_text()
|
||||
|
||||
|
||||
def make_executable(filename: str) -> None:
|
||||
|
|
@ -93,29 +50,26 @@ class CalledProcessError(RuntimeError):
|
|||
def __init__(
|
||||
self,
|
||||
returncode: int,
|
||||
cmd: Tuple[str, ...],
|
||||
expected_returncode: int,
|
||||
cmd: tuple[str, ...],
|
||||
stdout: bytes,
|
||||
stderr: Optional[bytes],
|
||||
stderr: bytes | None,
|
||||
) -> None:
|
||||
super().__init__(returncode, cmd, expected_returncode, stdout, stderr)
|
||||
super().__init__(returncode, cmd, stdout, stderr)
|
||||
self.returncode = returncode
|
||||
self.cmd = cmd
|
||||
self.expected_returncode = expected_returncode
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
def _indent_or_none(part: Optional[bytes]) -> bytes:
|
||||
def _indent_or_none(part: bytes | None) -> bytes:
|
||||
if part:
|
||||
return b'\n ' + part.replace(b'\n', b'\n ')
|
||||
return b'\n ' + part.replace(b'\n', b'\n ').rstrip()
|
||||
else:
|
||||
return b' (none)'
|
||||
|
||||
return b''.join((
|
||||
f'command: {self.cmd!r}\n'.encode(),
|
||||
f'return code: {self.returncode}\n'.encode(),
|
||||
f'expected return code: {self.expected_returncode}\n'.encode(),
|
||||
b'stdout:', _indent_or_none(self.stdout), b'\n',
|
||||
b'stderr:', _indent_or_none(self.stderr),
|
||||
))
|
||||
|
|
@ -124,24 +78,24 @@ class CalledProcessError(RuntimeError):
|
|||
return self.__bytes__().decode()
|
||||
|
||||
|
||||
def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None:
|
||||
def _setdefault_kwargs(kwargs: dict[str, Any]) -> None:
|
||||
for arg in ('stdin', 'stdout', 'stderr'):
|
||||
kwargs.setdefault(arg, subprocess.PIPE)
|
||||
|
||||
|
||||
def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]:
|
||||
def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]:
|
||||
return 1, force_bytes(e).rstrip(b'\n') + b'\n', None
|
||||
|
||||
|
||||
def cmd_output_b(
|
||||
*cmd: str,
|
||||
retcode: Optional[int] = 0,
|
||||
check: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Tuple[int, bytes, Optional[bytes]]:
|
||||
) -> tuple[int, bytes, bytes | None]:
|
||||
_setdefault_kwargs(kwargs)
|
||||
|
||||
try:
|
||||
cmd = parse_shebang.normalize_cmd(cmd)
|
||||
cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env'))
|
||||
except parse_shebang.ExecutableNotFoundError as e:
|
||||
returncode, stdout_b, stderr_b = e.to_output()
|
||||
else:
|
||||
|
|
@ -153,36 +107,36 @@ def cmd_output_b(
|
|||
stdout_b, stderr_b = proc.communicate()
|
||||
returncode = proc.returncode
|
||||
|
||||
if retcode is not None and retcode != returncode:
|
||||
raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
|
||||
if check and returncode:
|
||||
raise CalledProcessError(returncode, cmd, stdout_b, stderr_b)
|
||||
|
||||
return returncode, stdout_b, stderr_b
|
||||
|
||||
|
||||
def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]:
|
||||
def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]:
|
||||
returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs)
|
||||
stdout = stdout_b.decode() if stdout_b is not None else None
|
||||
stderr = stderr_b.decode() if stderr_b is not None else None
|
||||
return returncode, stdout, stderr
|
||||
|
||||
|
||||
if os.name != 'nt': # pragma: win32 no cover
|
||||
if sys.platform != 'win32': # pragma: win32 no cover
|
||||
from os import openpty
|
||||
import termios
|
||||
|
||||
class Pty:
|
||||
def __init__(self) -> None:
|
||||
self.r: Optional[int] = None
|
||||
self.w: Optional[int] = None
|
||||
self.r: int | None = None
|
||||
self.w: int | None = None
|
||||
|
||||
def __enter__(self) -> 'Pty':
|
||||
def __enter__(self) -> Pty:
|
||||
self.r, self.w = openpty()
|
||||
|
||||
# tty flags normally change \n to \r\n
|
||||
attrs = termios.tcgetattr(self.r)
|
||||
attrs = termios.tcgetattr(self.w)
|
||||
assert isinstance(attrs[1], int)
|
||||
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
|
||||
termios.tcsetattr(self.r, termios.TCSANOW, attrs)
|
||||
termios.tcsetattr(self.w, termios.TCSANOW, attrs)
|
||||
|
||||
return self
|
||||
|
||||
|
|
@ -198,19 +152,19 @@ if os.name != 'nt': # pragma: win32 no cover
|
|||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
self.close_w()
|
||||
self.close_r()
|
||||
|
||||
def cmd_output_p(
|
||||
*cmd: str,
|
||||
retcode: Optional[int] = 0,
|
||||
check: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> Tuple[int, bytes, Optional[bytes]]:
|
||||
assert retcode is None
|
||||
) -> tuple[int, bytes, bytes | None]:
|
||||
assert check is False
|
||||
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
|
||||
_setdefault_kwargs(kwargs)
|
||||
|
||||
|
|
@ -248,26 +202,38 @@ else: # pragma: no cover
|
|||
cmd_output_p = cmd_output_b
|
||||
|
||||
|
||||
def rmtree(path: str) -> None:
|
||||
"""On windows, rmtree fails for readonly dirs."""
|
||||
def handle_remove_readonly(
|
||||
func: Callable[..., Any],
|
||||
path: str,
|
||||
exc: Tuple[Type[OSError], OSError, TracebackType],
|
||||
def _handle_readonly(
|
||||
func: Callable[[str], object],
|
||||
path: str,
|
||||
exc: BaseException,
|
||||
) -> None:
|
||||
if (
|
||||
func in (os.rmdir, os.remove, os.unlink) and
|
||||
isinstance(exc, OSError) and
|
||||
exc.errno in {errno.EACCES, errno.EPERM}
|
||||
):
|
||||
for p in (path, os.path.dirname(path)):
|
||||
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
|
||||
func(path)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
if sys.version_info < (3, 12): # pragma: <3.12 cover
|
||||
def _handle_readonly_old(
|
||||
func: Callable[[str], object],
|
||||
path: str,
|
||||
excinfo: tuple[type[BaseException], BaseException, TracebackType],
|
||||
) -> None:
|
||||
excvalue = exc[1]
|
||||
if (
|
||||
func in (os.rmdir, os.remove, os.unlink) and
|
||||
excvalue.errno == errno.EACCES
|
||||
):
|
||||
for p in (path, os.path.dirname(path)):
|
||||
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
|
||||
func(path)
|
||||
else:
|
||||
raise
|
||||
shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly)
|
||||
return _handle_readonly(func, path, excinfo[1])
|
||||
|
||||
def rmtree(path: str) -> None:
|
||||
shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old)
|
||||
else: # pragma: >=3.12 cover
|
||||
def rmtree(path: str) -> None:
|
||||
"""On windows, rmtree fails for readonly dirs."""
|
||||
shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly)
|
||||
|
||||
|
||||
def parse_version(s: str) -> Tuple[int, ...]:
|
||||
"""poor man's version comparison"""
|
||||
return tuple(int(p) for p in s.split('.'))
|
||||
def win_exe(s: str) -> str:
|
||||
return s if sys.platform != 'win32' else f'{s}.exe'
|
||||
|
|
|
|||
|
|
@ -1,29 +1,44 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import contextlib
|
||||
import math
|
||||
import multiprocessing
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
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 typing import Callable
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TypeVar
|
||||
|
||||
from pre_commit import parse_shebang
|
||||
from pre_commit.util import cmd_output_b
|
||||
from pre_commit.util import cmd_output_p
|
||||
from pre_commit.util import EnvironT
|
||||
|
||||
TArg = TypeVar('TArg')
|
||||
TRet = TypeVar('TRet')
|
||||
|
||||
|
||||
def _environ_size(_env: Optional[EnvironT] = None) -> int:
|
||||
def cpu_count() -> int:
|
||||
try:
|
||||
# On systems that support it, this will return a more accurate count of
|
||||
# usable CPUs for the current process, which will take into account
|
||||
# cgroup limits
|
||||
return len(os.sched_getaffinity(0))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
return 1
|
||||
|
||||
|
||||
def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
|
||||
environ = _env if _env is not None else getattr(os, 'environb', os.environ)
|
||||
size = 8 * len(environ) # number of pointers in `envp`
|
||||
for k, v in environ.items():
|
||||
|
|
@ -62,8 +77,8 @@ def partition(
|
|||
cmd: Sequence[str],
|
||||
varargs: Sequence[str],
|
||||
target_concurrency: int,
|
||||
_max_length: Optional[int] = None,
|
||||
) -> Tuple[Tuple[str, ...], ...]:
|
||||
_max_length: int | None = None,
|
||||
) -> tuple[tuple[str, ...], ...]:
|
||||
_max_length = _max_length or _get_platform_max_length()
|
||||
|
||||
# Generally, we try to partition evenly into at least `target_concurrency`
|
||||
|
|
@ -73,7 +88,7 @@ def partition(
|
|||
cmd = tuple(cmd)
|
||||
ret = []
|
||||
|
||||
ret_cmd: List[str] = []
|
||||
ret_cmd: list[str] = []
|
||||
# Reversed so arguments are in order
|
||||
varargs = list(reversed(varargs))
|
||||
|
||||
|
|
@ -105,7 +120,6 @@ def partition(
|
|||
@contextlib.contextmanager
|
||||
def _thread_mapper(maxsize: int) -> Generator[
|
||||
Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
|
||||
None, None,
|
||||
]:
|
||||
if maxsize == 1:
|
||||
yield map
|
||||
|
|
@ -115,14 +129,14 @@ def _thread_mapper(maxsize: int) -> Generator[
|
|||
|
||||
|
||||
def xargs(
|
||||
cmd: Tuple[str, ...],
|
||||
cmd: tuple[str, ...],
|
||||
varargs: Sequence[str],
|
||||
*,
|
||||
color: bool = False,
|
||||
target_concurrency: int = 1,
|
||||
_max_length: int = _get_platform_max_length(),
|
||||
**kwargs: Any,
|
||||
) -> Tuple[int, bytes]:
|
||||
) -> tuple[int, bytes]:
|
||||
"""A simplified implementation of xargs.
|
||||
|
||||
color: Make a pty if on a platform that supports it
|
||||
|
|
@ -137,13 +151,25 @@ def xargs(
|
|||
except parse_shebang.ExecutableNotFoundError as e:
|
||||
return e.to_output()[:2]
|
||||
|
||||
# on windows, batch files have a separate length limit than windows itself
|
||||
if (
|
||||
sys.platform == 'win32' and
|
||||
cmd[0].lower().endswith(('.bat', '.cmd'))
|
||||
): # pragma: win32 cover
|
||||
# this is implementation details but the command gets translated into
|
||||
# full/path/to/cmd.exe /c *cmd
|
||||
cmd_exe = parse_shebang.find_executable('cmd.exe')
|
||||
# 1024 is additionally subtracted to give headroom for further
|
||||
# expansion inside the batch file
|
||||
_max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024
|
||||
|
||||
partitions = partition(cmd, varargs, target_concurrency, _max_length)
|
||||
|
||||
def run_cmd_partition(
|
||||
run_cmd: Tuple[str, ...],
|
||||
) -> Tuple[int, bytes, Optional[bytes]]:
|
||||
run_cmd: tuple[str, ...],
|
||||
) -> tuple[int, bytes, bytes | None]:
|
||||
return cmd_fn(
|
||||
*run_cmd, retcode=None, stderr=subprocess.STDOUT, **kwargs,
|
||||
*run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,
|
||||
)
|
||||
|
||||
threads = min(len(partitions), target_concurrency)
|
||||
|
|
@ -151,7 +177,8 @@ def xargs(
|
|||
results = thread_map(run_cmd_partition, partitions)
|
||||
|
||||
for proc_retcode, proc_out, _ in results:
|
||||
retcode = max(retcode, proc_retcode)
|
||||
if abs(proc_retcode) > abs(retcode):
|
||||
retcode = proc_retcode
|
||||
stdout += proc_out
|
||||
|
||||
return retcode, stdout
|
||||
|
|
|
|||
19
pre_commit/yaml.py
Normal file
19
pre_commit/yaml.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
|
||||
yaml_compose = functools.partial(yaml.compose, Loader=Loader)
|
||||
yaml_load = functools.partial(yaml.load, Loader=Loader)
|
||||
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
|
||||
|
||||
|
||||
def yaml_dump(o: Any, **kwargs: Any) -> str:
|
||||
# when python/mypy#1484 is solved, this can be `functools.partial`
|
||||
return yaml.dump(
|
||||
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
|
||||
**kwargs,
|
||||
)
|
||||
52
pre_commit/yaml_rewrite.py
Normal file
52
pre_commit/yaml_rewrite.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Iterable
|
||||
from typing import NamedTuple
|
||||
from typing import Protocol
|
||||
|
||||
from yaml.nodes import MappingNode
|
||||
from yaml.nodes import Node
|
||||
from yaml.nodes import ScalarNode
|
||||
from yaml.nodes import SequenceNode
|
||||
|
||||
|
||||
class _Matcher(Protocol):
|
||||
def match(self, n: Node) -> Generator[Node]: ...
|
||||
|
||||
|
||||
class MappingKey(NamedTuple):
|
||||
k: str
|
||||
|
||||
def match(self, n: Node) -> Generator[Node]:
|
||||
if isinstance(n, MappingNode):
|
||||
for k, _ in n.value:
|
||||
if k.value == self.k:
|
||||
yield k
|
||||
|
||||
|
||||
class MappingValue(NamedTuple):
|
||||
k: str
|
||||
|
||||
def match(self, n: Node) -> Generator[Node]:
|
||||
if isinstance(n, MappingNode):
|
||||
for k, v in n.value:
|
||||
if k.value == self.k:
|
||||
yield v
|
||||
|
||||
|
||||
class SequenceItem(NamedTuple):
|
||||
def match(self, n: Node) -> Generator[Node]:
|
||||
if isinstance(n, SequenceNode):
|
||||
yield from n.value
|
||||
|
||||
|
||||
def _match(gen: Iterable[Node], m: _Matcher) -> Iterable[Node]:
|
||||
return (n for src in gen for n in m.match(src))
|
||||
|
||||
|
||||
def match(n: Node, matcher: tuple[_Matcher, ...]) -> Generator[ScalarNode]:
|
||||
gen: Iterable[Node] = (n,)
|
||||
for m in matcher:
|
||||
gen = _match(gen, m)
|
||||
return (n for n in gen if isinstance(n, ScalarNode))
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
covdefaults
|
||||
covdefaults>=2.2
|
||||
coverage
|
||||
distlib
|
||||
pytest
|
||||
pytest-env
|
||||
re-assert
|
||||
|
|
|
|||
31
setup.cfg
31
setup.cfg
|
|
@ -1,6 +1,6 @@
|
|||
[metadata]
|
||||
name = pre_commit
|
||||
version = 2.5.0
|
||||
version = 4.5.1
|
||||
description = A framework for managing and maintaining multi-language pre-commit hooks.
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
|
@ -8,14 +8,10 @@ url = https://github.com/pre-commit/pre-commit
|
|||
author = Anthony Sottile
|
||||
author_email = asottile@umich.edu
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
classifiers =
|
||||
License :: OSI Approved :: MIT License
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Programming Language :: Python :: Implementation :: PyPy
|
||||
|
||||
|
|
@ -26,17 +22,17 @@ install_requires =
|
|||
identify>=1.0.0
|
||||
nodeenv>=0.11.1
|
||||
pyyaml>=5.1
|
||||
toml
|
||||
virtualenv>=20.0.8
|
||||
importlib-metadata;python_version<"3.8"
|
||||
importlib-resources;python_version<"3.7"
|
||||
python_requires = >=3.6.1
|
||||
virtualenv>=20.10.0
|
||||
python_requires = >=3.10
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests*
|
||||
testing*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pre-commit = pre_commit.main:main
|
||||
pre-commit-validate-config = pre_commit.clientlib:validate_config_main
|
||||
pre-commit-validate-manifest = pre_commit.clientlib:validate_manifest_main
|
||||
|
||||
[options.package_data]
|
||||
pre_commit.resources =
|
||||
|
|
@ -44,11 +40,6 @@ pre_commit.resources =
|
|||
empty_template_*
|
||||
hook-tmpl
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests*
|
||||
testing*
|
||||
|
||||
[bdist_wheel]
|
||||
universal = True
|
||||
|
||||
|
|
@ -61,7 +52,9 @@ check_untyped_defs = true
|
|||
disallow_any_generics = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
enable_error_code = deprecated
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
[mypy-testing.*]
|
||||
disallow_untyped_defs = false
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -1,2 +1,4 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from setuptools import setup
|
||||
setup()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os.path
|
||||
import shutil
|
||||
|
|
@ -10,8 +12,8 @@ from pre_commit import git
|
|||
from pre_commit.clientlib import CONFIG_SCHEMA
|
||||
from pre_commit.clientlib import load_manifest
|
||||
from pre_commit.util import cmd_output
|
||||
from pre_commit.util import yaml_dump
|
||||
from pre_commit.util import yaml_load
|
||||
from pre_commit.yaml import yaml_dump
|
||||
from pre_commit.yaml import yaml_load
|
||||
from testing.util import get_resource_path
|
||||
from testing.util import git_commit
|
||||
|
||||
|
|
@ -36,7 +38,7 @@ def copy_tree_to_path(src_dir, dest_dir):
|
|||
|
||||
def git_dir(tempdir_factory):
|
||||
path = tempdir_factory.get()
|
||||
cmd_output('git', 'init', path)
|
||||
cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path)
|
||||
return path
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
LANGUAGES = [
|
||||
'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl',
|
||||
'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system',
|
||||
]
|
||||
FIELDS = [
|
||||
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',
|
||||
'run_hook',
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(f' # BEGIN GENERATED ({sys.argv[0]})')
|
||||
for lang in LANGUAGES:
|
||||
parts = [f' {lang!r}: Language(name={lang!r}']
|
||||
for k in FIELDS:
|
||||
parts.append(f', {k}={lang}.{k}')
|
||||
parts.append('), # noqa: E501')
|
||||
print(''.join(parts))
|
||||
print(' # END GENERATED')
|
||||
return 0
|
||||
|
||||
|
||||
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