mirror of
https://github.com/watchexec/watchexec.git
synced 2024-11-16 17:18:30 +01:00
Compare commits
986 commits
Author | SHA1 | Date | |
---|---|---|---|
|
1c916a01cc | ||
|
51a70329c4 | ||
|
ffcdc1b536 | ||
|
d0dcbf0489 | ||
|
a0ca6294c6 | ||
|
cb2a5087ff | ||
|
ecc3256fa4 | ||
|
51c094b8a7 | ||
|
47d3f02ee3 | ||
|
e36799d9e3 | ||
|
9082f63114 | ||
|
303396e5a6 | ||
|
be3707abc1 | ||
|
4c9e72811f | ||
|
cc307828d2 | ||
|
0dafc4f260 | ||
|
967d53dfc0 | ||
|
71aa35a095 | ||
|
c46a06c263 | ||
|
792ebf6d04 | ||
|
ace8ea8c63 | ||
|
6c245d3aff | ||
|
08367c6db7 | ||
|
2cc7743677 | ||
|
2c1388e678 | ||
|
b0125bc113 | ||
|
510dd625d1 | ||
|
6e981fd8ab | ||
|
24df7a4f45 | ||
|
9f1f2e9d04 | ||
|
0e393c25cf | ||
|
2026c52abd | ||
|
72f069a847 | ||
|
4affed6fff | ||
|
e0084e69f8 | ||
|
592b712c95 | ||
|
c9a3b9df00 | ||
|
e63d37f601 | ||
|
14e6294f5a | ||
|
234d606563 | ||
|
77405c8ce1 | ||
|
6c23afe839 | ||
|
ee3795d776 | ||
|
eff96c7324 | ||
|
a4df258735 | ||
|
d388a280f0 | ||
|
bb97f71c8c | ||
|
953fa89dd9 | ||
|
0ef87821f2 | ||
|
62af5dd868 | ||
|
4497aaf515 | ||
|
a63864c5f2 | ||
|
ee815ba166 | ||
|
d6138b9961 | ||
|
f73d388d18 | ||
|
86d6c7d448 | ||
|
d317540fd3 | ||
|
9d91c51651 | ||
|
96480cb588 | ||
|
fd5afb8b3a | ||
|
e1cef25d7f | ||
|
22b58a66ab | ||
|
1c47ffbe1a | ||
|
48ff7ec68b | ||
|
4023bf7124 | ||
|
8864811e79 | ||
|
7535e17661 | ||
|
8ad12b1f65 | ||
|
dca13fed43 | ||
|
f81aed1260 | ||
|
ec316a7279 | ||
|
e505a9ad05 | ||
|
317221584a | ||
|
af24252f21 | ||
|
b72248a38c | ||
|
11b98f776a | ||
|
8c22d0cac7 | ||
|
338999eb65 | ||
|
538439f045 | ||
|
75b2c4b4ae | ||
|
a6e0b3f70a | ||
|
f538b74e81 | ||
|
a50ce396cb | ||
|
1846c96b86 | ||
|
8b39279423 | ||
|
7f4fba02ef | ||
|
d3949cc6e9 | ||
|
465ccfc597 | ||
|
6a2f637a60 | ||
|
4f757de8df | ||
|
217f57f6a2 | ||
|
682a9b4d21 | ||
|
447b6fa963 | ||
|
3cbf277b2e | ||
|
48793008eb | ||
|
1ef2fcebf1 | ||
|
8523bd196c | ||
|
cb1cfb6bf5 | ||
|
bf9c85f598 | ||
|
3ad0e1aa57 | ||
|
f65a0d21ba | ||
|
aa0bf1c24f | ||
|
0a6811f1fb | ||
|
e9cce54179 | ||
|
6ecc5569e4 | ||
|
eb4f2ce201 | ||
|
b4a64a096a | ||
|
a72ff0e142 | ||
|
53c9e344eb | ||
|
e4e8d39546 | ||
|
4026938c18 | ||
|
0557d70963 | ||
|
477d59d319 | ||
|
a166b3bc9f | ||
|
deb6072a26 | ||
|
709dbe5151 | ||
|
03460a6181 | ||
|
44d794c921 | ||
|
c75134d255 | ||
|
e90bf3756e | ||
|
91b34bc96e | ||
|
c4dceb2d88 | ||
|
16be20050b | ||
|
0e94f220e3 | ||
|
9af9189ac4 | ||
|
16e606e944 | ||
|
dc91e69966 | ||
|
2751caeb39 | ||
|
e7580b0a35 | ||
|
63562fe64d | ||
|
d9f6d20b6b | ||
|
fb2c5449af | ||
|
64bdf7c9d5 | ||
|
65e2db31bc | ||
|
f66aa5d808 | ||
|
efacb29f86 | ||
|
10556dea11 | ||
|
87ca5d6b20 | ||
|
318f850e9b | ||
|
b54cd60146 | ||
|
89e3d60ecf | ||
|
a13bc429eb | ||
|
7f23fbd68a | ||
|
652b8c9b2f | ||
|
19a3fc9189 | ||
|
3639f06745 | ||
|
1aaff8d319 | ||
|
345eda871b | ||
|
4c3b9f0960 | ||
|
96bf3d231e | ||
|
afe317d4ee | ||
|
a91d80141b | ||
|
402d1ba4f9 | ||
|
0f2f5f4ea6 | ||
|
f08002aaf6 | ||
|
d72fc38e62 | ||
|
2a3a3dee5f | ||
|
6552e3cd7e | ||
|
31b9c2fb91 | ||
|
d25ae71787 | ||
|
638cddcf83 | ||
|
eb19f83761 | ||
|
1d87097b3e | ||
|
7c1c726d85 | ||
|
0b50c91a1c | ||
|
03b3aef9b7 | ||
|
0a1e5781cc | ||
|
af2e1562f1 | ||
|
1028caa389 | ||
|
6a86caacf2 | ||
|
0afb5f2796 | ||
|
3e79957ad3 | ||
|
e6f2dcdf48 | ||
|
f9042808ab | ||
|
299a01dcf6 | ||
|
b9c052d10e | ||
|
bccc0292a0 | ||
|
8e91d26ef6 | ||
|
e8391cd1d0 | ||
|
8a2808027f | ||
|
38109b8128 | ||
|
7e06e2d91d | ||
|
4d081848a7 | ||
|
1979d4400d | ||
|
7330827db4 | ||
|
955562062f | ||
|
4294bd42da | ||
|
f0d115f600 | ||
|
ce9c5d6b22 | ||
|
59044a5cba | ||
|
483b29af8e | ||
|
c166823cb6 | ||
|
79b28d0264 | ||
|
e06dc0dd16 | ||
|
f15ea8a825 | ||
|
bfb0a0648a | ||
|
c337ebca86 | ||
|
a3ebfcceef | ||
|
cba452edf2 | ||
|
7444ec133f | ||
|
26679721aa | ||
|
d8f997e104 | ||
|
d6ae98e644 | ||
|
9ae16187f0 | ||
|
5c63a99013 | ||
|
80e18ea145 | ||
|
8156864bf3 | ||
|
64ad4e31f5 | ||
|
7383679fbc | ||
|
d866ee615f | ||
|
f83790bce3 | ||
|
d5fb5bfe26 | ||
|
0fe17e036a | ||
|
639bef9e31 | ||
|
690b32f030 | ||
|
f76b16c4f4 | ||
|
14f53a6bf5 | ||
|
1e72cf6866 | ||
|
c87a7343b3 | ||
|
e841333c07 | ||
|
70beb51ab5 | ||
|
46453a2b35 | ||
|
6232c213a9 | ||
|
ca0c300193 | ||
|
b8a8b2265e | ||
|
d4b86366a2 | ||
|
84d3947247 | ||
|
5850c284ca | ||
|
90eb9cae03 | ||
|
2c06f06ee1 | ||
|
d6b1d36c6e | ||
|
d92a7eb675 | ||
|
950f302ebf | ||
|
19f5bd0d1e | ||
|
2479aaec4f | ||
|
4e3e7a140a | ||
|
daa11aad09 | ||
|
7ba37b3964 | ||
|
7a8680e937 | ||
|
ac2a14fd80 | ||
|
2523bf5a65 | ||
|
33f5ba5ef9 | ||
|
f55d669e3c | ||
|
a23b924be2 | ||
|
404386e795 | ||
|
5d0920c5c9 | ||
|
f3305c5484 | ||
|
2387318ea2 | ||
|
50c1832b35 | ||
|
60a5992e7a | ||
|
97694c05f5 | ||
|
81d59b62bc | ||
|
af5742945b | ||
|
dc98370492 | ||
|
6d65c05e35 | ||
|
75da1a61ed | ||
|
04a8ee22bd | ||
|
f613ba1a79 | ||
|
db287c49f7 | ||
|
e6bde7a48d | ||
|
c30fd84c02 | ||
|
b25cf1586c | ||
|
46fba794c1 | ||
|
d350f91b12 | ||
|
bb44b3d2dc | ||
|
ec1cf13a41 | ||
|
6f4c8604e8 | ||
|
cf80b8395f | ||
|
c7c9b30636 | ||
|
695b36e345 | ||
|
4397aa5c7e | ||
|
dd882109f8 | ||
|
014e6246ac | ||
|
8b4e5ef71f | ||
|
236fac4c99 | ||
|
b2ea64d436 | ||
|
4ef180f486 | ||
|
6642362025 | ||
|
d168ed9676 | ||
|
91a12383fd | ||
|
c46187f92d | ||
|
24a3a3c2f2 | ||
|
669ad6a7bf | ||
|
f7d38206e3 | ||
|
6c40113e9e | ||
|
1042cba39c | ||
|
79bcf6838d | ||
|
b2b3bb8fde | ||
|
a49b38c9fd | ||
|
e9d24688e1 | ||
|
b2de641f20 | ||
|
475063c49f | ||
|
05458c3452 | ||
|
174af8665f | ||
|
f684db57ee | ||
|
ce68338381 | ||
|
94deab9452 | ||
|
34184f925a | ||
|
f866e6f498 | ||
|
bcba352603 | ||
|
90c601deeb | ||
|
bb9021eec7 | ||
|
ea9f5ff694 | ||
|
c0c9193dff | ||
|
f0d64f07df | ||
|
f548fd1956 | ||
|
f9ec29a60b | ||
|
96825a6f5a | ||
|
e15fa6cd21 | ||
|
64895132aa | ||
|
1b70a92162 | ||
|
1d2dd7b904 | ||
|
94f4dd26c2 | ||
|
5079f45198 | ||
|
50e2f0acbf | ||
|
4970646f1a | ||
|
3aa128c4ee | ||
|
75784667bc | ||
|
ede4959102 | ||
|
77d4b03398 | ||
|
d71412cd12 | ||
|
9c343a6eb2 | ||
|
69464d373f | ||
|
5cf89eb7b4 | ||
|
6c3cb476a8 | ||
|
4f808a1002 | ||
|
c3df998895 | ||
|
0e13595f08 | ||
|
334c704cc4 | ||
|
d79a080ad1 | ||
|
5c2c80a6e0 | ||
|
92d05755c9 | ||
|
adc4a0a576 | ||
|
f0e05f9526 | ||
|
ba86225ad4 | ||
|
361e5530c5 | ||
|
4562c0f2f9 | ||
|
03ee184d41 | ||
|
e1d1940261 | ||
|
5ee0356e34 | ||
|
b133486c66 | ||
|
18dea4b6e6 | ||
|
e98989f099 | ||
|
f21b45e6aa | ||
|
35cf63bc85 | ||
|
70aa48c59d | ||
|
ae2718ec4d | ||
|
0e1a6f2c45 | ||
|
446a8d95a7 | ||
|
62e79fbf7a | ||
|
557efab1c4 | ||
|
e215e2c09f | ||
|
ec26a99b7d | ||
|
f3e0f0cbda | ||
|
84eb92b82e | ||
|
3d19ce04e7 | ||
|
d395faeefa | ||
|
4be340a4bc | ||
|
edf023c009 | ||
|
c58d66bec5 | ||
|
396424112e | ||
|
3523925c1e | ||
|
f60a623a44 | ||
|
6f3cdac9fd | ||
|
515d5568b2 | ||
|
84aac2243c | ||
|
4a6d4350c5 | ||
|
00e9b17043 | ||
|
d4acb9d719 | ||
|
009ef2356f | ||
|
b374de6097 | ||
|
6b0f626441 | ||
|
036d9a27e8 | ||
|
98348be8b5 | ||
|
20597802ad | ||
|
e8d3bb964c | ||
|
386e01b06f | ||
|
57e2ddf9fa | ||
|
d38428f297 | ||
|
7baf79e929 | ||
|
3969264e91 | ||
|
71f2e38ecb | ||
|
07bf2523b6 | ||
|
572fc53793 | ||
|
bd3d7a8411 | ||
|
29dcd418ba | ||
|
e5731abde0 | ||
|
aeb699b658 | ||
|
da22d34274 | ||
|
17edfe663b | ||
|
223d29fa4a | ||
|
465895ddec | ||
|
5e6adabf8d | ||
|
a3c03b9fac | ||
|
ce449d85e2 | ||
|
9ff5875327 | ||
|
72100217e8 | ||
|
03aef187bd | ||
|
4af6865ef0 | ||
|
93da1ce780 | ||
|
b77da446d8 | ||
|
3b64a41d80 | ||
|
56380154d3 | ||
|
797a7fc708 | ||
|
e5bb99a60b | ||
|
bd7a933906 | ||
|
bdcbe5ca3c | ||
|
55c02567e0 | ||
|
96a6a6e9d9 | ||
|
94b21c2da3 | ||
|
965030b47b | ||
|
e40744f033 | ||
|
c6f230a168 | ||
|
427e4a0d08 | ||
|
8ebcf083b8 | ||
|
a890b0c916 | ||
|
ba64fb3c38 | ||
|
ada9888fdf | ||
|
bc50c198b3 | ||
|
d7a305fc4c | ||
|
2a9ee4de0b | ||
|
9a736c5eb9 | ||
|
995d38078e | ||
|
3e942c4d19 | ||
|
fdf4dcd13e | ||
|
12ef0411f8 | ||
|
328700dd72 | ||
|
c74eab5e91 | ||
|
cc0c0be45a | ||
|
22f081a47b | ||
|
10b31a0269 | ||
|
a651d149b0 | ||
|
b8ffa0c38a | ||
|
9845d48464 | ||
|
d9006a240d | ||
|
c444895593 | ||
|
18a2b204e2 | ||
|
c79d2726ba | ||
|
7ccd3c92e0 | ||
|
e6d80558f8 | ||
|
fc40336db7 | ||
|
a6c997f470 | ||
|
366277c6c1 | ||
|
96a41e1b4c | ||
|
77ee59a9e3 | ||
|
d067b5a5de | ||
|
f6a3b76d92 | ||
|
a5e756df7c | ||
|
59d8388d7e | ||
|
42d64d0a6d | ||
|
6cebaf204f | ||
|
94b4dffc02 | ||
|
8c8cdf8e82 | ||
|
5bdd87dbf6 | ||
|
67452152c2 | ||
|
a51979b54e | ||
|
9b866ad9e8 | ||
|
b8b2c2ca70 | ||
|
b5c16f291e | ||
|
4e7e75e1d5 | ||
|
c79a3bd12c | ||
|
187c696d8f | ||
|
7effd1a61e | ||
|
7d60b1689e | ||
|
a3fd743786 | ||
|
631c328a55 | ||
|
c58333496e | ||
|
c367288e38 | ||
|
c91ab99ac3 | ||
|
9f67ec35b1 | ||
|
881e8f4047 | ||
|
0334b9c9fa | ||
|
7f092ca4b7 | ||
|
783da395bb | ||
|
8748c062c4 | ||
|
fd474c7e38 | ||
|
044a8244f9 | ||
|
1d64cac11f | ||
|
8bd48c6290 | ||
|
4737937350 | ||
|
e2f27a1b01 | ||
|
2ea62aec6a | ||
|
bc0fe6be70 | ||
|
93a961abf6 | ||
|
4085fc522c | ||
|
44a44a50d1 | ||
|
b29b3bf1e0 | ||
|
c821faf383 | ||
|
6a925fc392 | ||
|
2a1a386208 | ||
|
26e0b60b7b | ||
|
10b71847da | ||
|
9327f8c293 | ||
|
2682dde41d | ||
|
3d32f2d24b | ||
|
561a34d90e | ||
|
817f4c2caf | ||
|
b8d9ad728e | ||
|
d6dfb87063 | ||
|
8219e577d0 | ||
|
f073eeb344 | ||
|
0618eb45fc | ||
|
ed6df57aa0 | ||
|
1c388d6b2d | ||
|
0c2b4848be | ||
|
e40b426930 | ||
|
63f9eedfec | ||
|
e9bc4fcf1b | ||
|
5bdc1b491f | ||
|
58d47f78e1 | ||
|
1ce238949e | ||
|
f0435eeaf3 | ||
|
431ef710ee | ||
|
97070c815c | ||
|
d70abeac6f | ||
|
78b8acf6cc | ||
|
afb61712a6 | ||
|
be5782432a | ||
|
da6f9cfc55 | ||
|
775b0ac81c | ||
|
5e285074bd | ||
|
00def9e19a | ||
|
2c840f4a2d | ||
|
5f43a6eae8 | ||
|
0e12030e3b | ||
|
23a3482361 | ||
|
0936a68355 | ||
|
ca4d4900a9 | ||
|
31cbe69051 | ||
|
9527202c51 | ||
|
6a541e5f27 | ||
|
1c99546071 | ||
|
163bcc3022 | ||
|
7fa657fc48 | ||
|
7fa80f8715 | ||
|
d0bb14f39c | ||
|
d39ffed51e | ||
|
185676cc1e | ||
|
9ed3645088 | ||
|
53d9a65e5f | ||
|
ed72c4998e | ||
|
4dbb924977 | ||
|
b5da4e31ba | ||
|
736f7f1270 | ||
|
a00912f17f | ||
|
e2f6fe147a | ||
|
be4b184cd0 | ||
|
b300be081f | ||
|
ea3f09408f | ||
|
ffcdf17914 | ||
|
c45840b657 | ||
|
db322f1228 | ||
|
75f14ba3d6 | ||
|
08a1e7cc67 | ||
|
6f8049dd93 | ||
|
6cd2252b6c | ||
|
bb212b413f | ||
|
ec49185488 | ||
|
3bfbcaaa2f | ||
|
3f94c3c088 | ||
|
d64135c2c0 | ||
|
c3094eaff8 | ||
|
0f51a6e794 | ||
|
26254f7022 | ||
|
8f9492a7bc | ||
|
f19dbf945d | ||
|
9d04143202 | ||
|
401b84db8a | ||
|
d14e7ff41b | ||
|
8a7699cf2f | ||
|
e4d669e230 | ||
|
e7de00edfe | ||
|
5856f976db | ||
|
4323a28852 | ||
|
ff8b019245 | ||
|
8a25ea95af | ||
|
962c7cd6d6 | ||
|
02534defd6 | ||
|
946c52b513 | ||
|
159a8a4b2e | ||
|
ba9d9939d9 | ||
|
5ebceddc3e | ||
|
d38a78631f | ||
|
debded9c0e | ||
|
d6b5dd5ae0 | ||
|
b59acaa9cf | ||
|
e4cd1d7379 | ||
|
1af3a1c849 | ||
|
23925db51e | ||
|
015447e433 | ||
|
a4c863baf8 | ||
|
f8250f47de | ||
|
289f76ae6c | ||
|
f229456f88 | ||
|
89fd99a683 | ||
|
e9275702d1 | ||
|
f304774a4f | ||
|
a3751519e8 | ||
|
41869af688 | ||
|
0826d531af | ||
|
ce3f2a3cbd | ||
|
2d633d9177 | ||
|
1ff3cbf455 | ||
|
f3bc5fa6d3 | ||
|
4367380a7c | ||
|
269894e1b0 | ||
|
0efdf8ea16 | ||
|
a46cfba1a2 | ||
|
a12a83cea2 | ||
|
745e3baa71 | ||
|
631b492064 | ||
|
afd9677441 | ||
|
b13c23c576 | ||
|
6d23339dea | ||
|
5c012c5b0c | ||
|
963cd68cc4 | ||
|
470cdd698b | ||
|
6671863f2f | ||
|
401437784d | ||
|
be37349b90 | ||
|
c84e14825d | ||
|
c758675728 | ||
|
110e1d4c96 | ||
|
34d7c5ee9c | ||
|
f9cbb11258 | ||
|
9d14ad7166 | ||
|
96de715cf7 | ||
|
beb87bdbb0 | ||
|
489fb612c7 | ||
|
1eb10074ef | ||
|
43353fecb2 | ||
|
0b60f5edb5 | ||
|
a79de88c58 | ||
|
5d57621512 | ||
|
70b1a3cd7b | ||
|
60fa09182d | ||
|
fae4fbf9a0 | ||
|
17b83fda08 | ||
|
fcf6a2154a | ||
|
88dfc0d664 | ||
|
1522aaf409 | ||
|
d43165494f | ||
|
47fc2f30a8 | ||
|
0ad0845018 | ||
|
8b8b9674aa | ||
|
423caaef2a | ||
|
e62f313533 | ||
|
2225b6d097 | ||
|
17310ee4b5 | ||
|
e06f615531 | ||
|
f58e97a62f | ||
|
5d2f2fcf62 | ||
|
30abed3fb2 | ||
|
ebabef9eed | ||
|
19b27959ed | ||
|
b2f4d0f244 | ||
|
083c1e2f52 | ||
|
e577b040b9 | ||
|
92513a4dc3 | ||
|
86882e8d27 | ||
|
75243bfdad | ||
|
55e4e1dc58 | ||
|
b780345e8b | ||
|
948388b019 | ||
|
8f61ac31da | ||
|
14b0364135 | ||
|
ae6af17aea | ||
|
87b6729ab7 | ||
|
f24e95504b | ||
|
c6336cdf3c | ||
|
fb4f136c0d | ||
|
758ac2dc89 | ||
|
fd2edbf11c | ||
|
c9da2c133a | ||
|
f16ba2dff1 | ||
|
cd7d5f1fcb | ||
|
3c65aee839 | ||
|
1dff1f5644 | ||
|
59fe74656a | ||
|
7af0339871 | ||
|
3219be53f5 | ||
|
65b042ec8f | ||
|
8bc58ba6b5 | ||
|
a1fce1b06e | ||
|
323f2d29ee | ||
|
3dff065f4b | ||
|
81bee9513d | ||
|
07878f8357 | ||
|
288ce9d2f4 | ||
|
d6b7175bb1 | ||
|
f93ba29982 | ||
|
dfb5525c7e | ||
|
e916ac8050 | ||
|
45a7ce6aa0 | ||
|
6b306a15ab | ||
|
30dae61a02 | ||
|
7cdb6ac5ad | ||
|
f492bca8c3 | ||
|
9bb6e1356a | ||
|
f673d00e9c | ||
|
f3c74bd151 | ||
|
4fda3c477b | ||
|
6e414d1de4 | ||
|
2c894266a8 | ||
|
b57fa8b236 | ||
|
7875b4db67 | ||
|
f1685821a3 | ||
|
29d0b66ba9 | ||
|
2be21b6bac | ||
|
84dc77f787 | ||
|
6a55f5cc6d | ||
|
9e3c8c1f32 | ||
|
59647e64a6 | ||
|
c2d57fcf48 | ||
|
e5fcc6553e | ||
|
4043ed34ae | ||
|
645ab74c62 | ||
|
b923638cbd | ||
|
9c8d4c1a1b | ||
|
07f5e445f6 | ||
|
5cbbb7b67f | ||
|
608aa516b1 | ||
|
29e7780fdc | ||
|
ec6b508894 | ||
|
b728bfecfc | ||
|
f880b0b38a | ||
|
8e4994abca | ||
|
0f247e9e5c | ||
|
ef453193af | ||
|
1fd5c85317 | ||
|
6df6d6fd5a | ||
|
7d492fa677 | ||
|
70e8a4dff2 | ||
|
58b37940b8 | ||
|
6767948daa | ||
|
33fb691d29 | ||
|
9cb1f5bf79 | ||
|
e939f97c90 | ||
|
23d794ed7e | ||
|
33f8b60e46 | ||
|
05afb141b6 | ||
|
b4ead7f5fb | ||
|
3588cb4d62 | ||
|
6cb3fc1c9a | ||
|
4a9168f5e6 | ||
|
9b94f18890 | ||
|
b42336cb74 | ||
|
b2effda341 | ||
|
5314d201a4 | ||
|
74d8e73817 | ||
|
931648a955 | ||
|
cc5b1c988e | ||
|
60ed6bba25 | ||
|
05117e69fe | ||
|
4843920a36 | ||
|
17b09d8798 | ||
|
f4a8a9fc6a | ||
|
18d2487ec3 | ||
|
3066ee5913 | ||
|
f150c26b22 | ||
|
d2d0bb7be2 | ||
|
7a709b9b4d | ||
|
c85c164c09 | ||
|
227c2a0e0d | ||
|
613fe24c64 | ||
|
8998c40746 | ||
|
d7d549a4c8 | ||
|
6c3c06e39c | ||
|
53854d93d4 | ||
|
656c0d8fd2 | ||
|
40f6f7397f | ||
|
5b8611a8c0 | ||
|
350b85e0c7 | ||
|
2a0661b122 | ||
|
2812a723ff | ||
|
ce60be2ec9 | ||
|
f2f138ce9f | ||
|
019018c93e | ||
|
816313303a | ||
|
0f37e42243 | ||
|
dcde429787 | ||
|
9f34492c79 | ||
|
95ad3e91ff | ||
|
6a46c2bff3 | ||
|
249c581dc9 | ||
|
5d11ccaa71 | ||
|
319729582f | ||
|
6f3abdeaea | ||
|
4e4a8e6853 | ||
|
0bb38f40a5 | ||
|
826dbd8cda | ||
|
cae00070fb | ||
|
0237a568df | ||
|
f5e19a6e5f | ||
|
7053360187 | ||
|
822148da03 | ||
|
f08bdad8ee | ||
|
61fec2cf27 | ||
|
b15615bbaa | ||
|
e21a3a99f6 | ||
|
13140c88c4 | ||
|
42c34a66a2 | ||
|
c6ec9015f7 | ||
|
d54d74854e | ||
|
a3173194a1 | ||
|
3208652ed8 | ||
|
aa99fa1674 | ||
|
bc07ad486d | ||
|
3948e4e9a0 | ||
|
d7b7dcd5f0 | ||
|
288eba37e0 | ||
|
bd96803d1e | ||
|
0ab1405186 | ||
|
7bd99f30df | ||
|
bf922e36ac | ||
|
d8fb70c454 | ||
|
66caedf978 | ||
|
ef36d3429d | ||
|
73eef67206 | ||
|
3485bd7e16 | ||
|
f88c30b854 | ||
|
18fdbbcfea | ||
|
85e2c12c34 | ||
|
146d49ee8e | ||
|
665f5ef6f0 | ||
|
d59a76182a | ||
|
392547ffa4 | ||
|
f5a92220c9 | ||
|
eb59e92b8f | ||
|
bb6a5ae891 | ||
|
72cda2b0b0 | ||
|
9c20c8c8b5 | ||
|
4d6ad2cc1f | ||
|
07974e0d14 | ||
|
847520357c | ||
|
54ac2e9ae0 | ||
|
0c25ea0269 | ||
|
6457df5a0a | ||
|
8368846444 | ||
|
7b16a226b2 | ||
|
ba26999028 | ||
|
604c24ec38 | ||
|
e6c7da3635 | ||
|
1eaaf7e844 | ||
|
4526ba2cae | ||
|
654a324fdd | ||
|
08d795f841 | ||
|
badb27a19c | ||
|
bdc2e384fc | ||
|
585f73adde | ||
|
71a178d4c2 | ||
|
960bbbabbc | ||
|
695c2e0f1a | ||
|
f0862b9f1e | ||
|
d8540d76b8 | ||
|
319727b0a6 | ||
|
7e4ff78bc4 | ||
|
169de99ee5 | ||
|
1015c1979a | ||
|
cb684fc7a8 | ||
|
0d2f28c91a | ||
|
eafbc9a77b | ||
|
324fbe9055 | ||
|
668d931eac | ||
|
6e844cc29a | ||
|
dc4a31252f | ||
|
be16d2e6a8 | ||
|
9a002be54c | ||
|
ad9f51a24b | ||
|
c517b98782 | ||
|
6a0ac73547 | ||
|
c5c69731c2 | ||
|
8ebbce5516 | ||
|
d4e0e93278 | ||
|
148be31f65 | ||
|
8295ac7a1c | ||
|
6aebd9b3e7 | ||
|
07f56ac7d5 | ||
|
d23261673b | ||
|
bdc3550a0d | ||
|
149b9b9821 | ||
|
aeab4159d4 | ||
|
b761118c19 | ||
|
181ca7b5c5 | ||
|
aff9f2d8f2 | ||
|
1f3f4ada35 | ||
|
898f83e29c | ||
|
5da59b122d | ||
|
74934f304f | ||
|
db5b662581 | ||
|
74cec51700 | ||
|
6e2835b8aa | ||
|
12184136f7 | ||
|
bed43d9e4c | ||
|
97207710cd | ||
|
1fab379cbd | ||
|
0db6c97f2d | ||
|
f30e2fc49e | ||
|
73ac954a40 | ||
|
1a8d1cb142 | ||
|
fe4410640d | ||
|
226ef97ad9 | ||
|
150c474b9c | ||
|
fa081d97bb | ||
|
8decbd63f6 | ||
|
1b889b3063 | ||
|
34989e6fc2 | ||
|
a510ecd936 | ||
|
cdcb4e2af4 | ||
|
2603f13fc7 | ||
|
25f02c5777 | ||
|
1c338adb9d | ||
|
81b3340f6c | ||
|
0e26934bbd | ||
|
591b4a11c2 | ||
|
749bf89b6c | ||
|
f1c7b555e1 | ||
|
6635635a9c | ||
|
1b44e7aa87 | ||
|
a2078e3703 | ||
|
3c26e3987c | ||
|
10429777ce | ||
|
940f4b98d0 | ||
|
139b6b43c7 | ||
|
03aad1caaa | ||
|
579e53678e | ||
|
d124fdb587 | ||
|
6b1ba7aad7 | ||
|
05e66784ce | ||
|
6ec02d38ab | ||
|
b5f56e831d | ||
|
f4637892ab | ||
|
b0d2a6c875 | ||
|
15cdd7c754 | ||
|
75ef0095ff | ||
|
5da11d2fd7 | ||
|
52284ae866 | ||
|
52ca1e98fb | ||
|
d37261529c | ||
|
15806019af | ||
|
0ade9dfc3a | ||
|
2e85658a12 | ||
|
5753e7773f | ||
|
cdd8a7b91a | ||
|
ced37d7617 | ||
|
e08f1934ec | ||
|
d25c374d7f | ||
|
f32943bfd2 | ||
|
1b5bdee08e | ||
|
12d130c3f1 | ||
|
6cfe649228 | ||
|
e5cdd51633 | ||
|
4c29c7a09d | ||
|
92060e5655 | ||
|
1280a15ca2 | ||
|
2a382a9486 | ||
|
7965ccb605 | ||
|
bbaaff8a1e | ||
|
cd3b8c6cba | ||
|
31fd818247 | ||
|
757de0d92e | ||
|
0c6b1bf2e4 | ||
|
988acaed65 | ||
|
9c1b65e712 | ||
|
7d523dfcf0 | ||
|
c42fe66985 | ||
|
426b6a8318 | ||
|
9968a66042 | ||
|
268c0cee38 | ||
|
0b79e93c84 | ||
|
fca038b23c | ||
|
1da3a8d26c | ||
|
ecd7d1c7e3 | ||
|
3a3ae89046 | ||
|
5e4af976de | ||
|
949fbcf7ae | ||
|
35e39afb95 | ||
|
f99a1a7a38 | ||
|
6414f60b52 | ||
|
c8619e34e0 | ||
|
838103fcf2 | ||
|
166f0b8fc2 | ||
|
f2da804d3b | ||
|
55b03e82c3 | ||
|
570ed3afdf | ||
|
fdb0cfb759 |
242 changed files with 29596 additions and 3902 deletions
11
.cargo/config.toml
Normal file
11
.cargo/config.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[target.armv7-unknown-linux-gnueabihf]
|
||||
linker = "arm-linux-gnueabihf-gcc"
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "arm-linux-musleabihf-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
12
.clippy-lints
Normal file
12
.clippy-lints
Normal file
|
@ -0,0 +1,12 @@
|
|||
-W clippy::nursery
|
||||
-W clippy::pedantic
|
||||
-A clippy::module-name-repetitions
|
||||
-A clippy::similar-names
|
||||
-A clippy::cognitive-complexity
|
||||
-A clippy::too-many-lines
|
||||
-A clippy::missing-errors-doc
|
||||
-A clippy::missing-panics-doc
|
||||
-A clippy::default-trait-access
|
||||
-A clippy::enum-glob-use
|
||||
-A clippy::option-if-let-else
|
||||
-A clippy::blocks-in-conditions
|
|
@ -1,26 +1,21 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[*.toml]
|
||||
indent_size = 2
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
|
||||
[*.wat]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
[cli/tests/snapshots/*]
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{md,ronn}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{cff,yml}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 3
|
||||
|
|
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
Cargo.lock merge=binary
|
||||
doc/watchexec.* merge=binary
|
||||
completions/* merge=binary
|
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Something is wrong
|
||||
title: ''
|
||||
labels: bug, need-info
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please delete this template text before filing, but you _need_ to include the following:
|
||||
|
||||
- Watchexec's version
|
||||
- The OS you're using
|
||||
- A log with `-vvv --log-file` (if it has sensitive info you can email it at felix@passcod.name — do that _after_ filing so you can reference the issue ID)
|
||||
- A sample command that you've run that has the issue
|
||||
|
||||
Thank you
|
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Something is missing
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please note that this project has a high threshold for changing default behaviour or breaking compatibility. If your feature or change can be done without breaking, present it that way. -->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
If proposing a new CLI option, option names you think would fit.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the feature request here.
|
23
.github/ISSUE_TEMPLATE/regression.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/regression.md
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
name: Regression
|
||||
about: Something changed unexpectedly
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What used to happen**
|
||||
|
||||
**What happens now**
|
||||
|
||||
**Details**
|
||||
- Latest version that worked:
|
||||
- Earliest version that doesn't: (don't sweat testing earlier versions if you don't remember or have time, your current version will do)
|
||||
- OS:
|
||||
- A debug log with `-vvv --log-file`:
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
<!-- You may truncate the log to just the part supporting your report if you're confident the rest is irrelevant. If it contains sensitive information (if you can't reduce/reproduce outside of work you'd rather remain private, you can either redact it or send it by email.) -->
|
50
.github/dependabot.yml
vendored
Normal file
50
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Dependabot dependency version checks / updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
# Workflow files stored in the
|
||||
# default location of `.github/workflows`
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/cli"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/lib"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/events"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/signals"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/supervisor"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/filterer/ignore"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/filterer/globset"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/bosion"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/ignore-files"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/crates/project-origins"
|
||||
schedule:
|
||||
interval: "weekly"
|
62
.github/workflows/clippy.yml
vendored
Normal file
62
.github/workflows/clippy.yml
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
name: Clippy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags-ignore:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
clippy:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- ubuntu
|
||||
- windows
|
||||
- macos
|
||||
|
||||
name: Clippy on ${{ matrix.platform }}
|
||||
runs-on: "${{ matrix.platform }}-latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure toolchain
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal --no-self-update --component clippy
|
||||
rustup default stable
|
||||
|
||||
# https://github.com/actions/cache/issues/752
|
||||
- if: ${{ runner.os == 'Windows' }}
|
||||
name: Use GNU tar
|
||||
shell: cmd
|
||||
run: |
|
||||
echo "Adding GNU tar to PATH"
|
||||
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
|
||||
|
||||
- name: Configure caching
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy -- $(cat .clippy-lints | tr -d '\r' | xargs)
|
||||
shell: bash
|
26
.github/workflows/dist-manifest.jq
vendored
Normal file
26
.github/workflows/dist-manifest.jq
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
dist_version: "0.0.2",
|
||||
releases: [{
|
||||
app_name: "watchexec",
|
||||
app_version: $version,
|
||||
changelog_title: "CLI \($version)",
|
||||
artifacts: [ $files | split("\n") | .[] | {
|
||||
name: .,
|
||||
kind: (if (. | test("[.](deb|rpm)$")) then "installer" else "executable-zip" end),
|
||||
target_triples: (. | [capture("watchexec-[^-]+-(?<target>[^.]+)[.].+").target]),
|
||||
assets: ([[
|
||||
{
|
||||
kind: "executable",
|
||||
name: (if (. | test("windows")) then "watchexec.exe" else "watchexec" end),
|
||||
path: "\(
|
||||
capture("(?<dir>watchexec-[^-]+-[^.]+)[.].+").dir
|
||||
)\(
|
||||
if (. | test("windows")) then "\\watchexec.exe" else "/watchexec" end
|
||||
)",
|
||||
},
|
||||
(if (. | test("[.](deb|rpm)$")) then null else {kind: "readme", name: "README.md"} end),
|
||||
(if (. | test("[.](deb|rpm)$")) then null else {kind: "license", name: "LICENSE"} end)
|
||||
][] | select(. != null)])
|
||||
} ]
|
||||
}]
|
||||
}
|
325
.github/workflows/release-cli.yml
vendored
Normal file
325
.github/workflows/release-cli.yml
vendored
Normal file
|
@ -0,0 +1,325 @@
|
|||
name: CLI Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
|
||||
|
||||
jobs:
|
||||
info:
|
||||
name: Gather info
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cli_version: ${{ steps.version.outputs.cli_version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Extract version
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
version=$(grep -m1 -F 'version =' crates/cli/Cargo.toml | cut -d\" -f2)
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "Error: no version :("
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "cli_version=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
name:
|
||||
- linux-amd64-gnu
|
||||
- linux-amd64-musl
|
||||
- linux-i686-musl
|
||||
- linux-armhf-gnu
|
||||
- linux-arm64-gnu
|
||||
- linux-arm64-musl
|
||||
- linux-s390x-gnu
|
||||
- linux-ppc64le-gnu
|
||||
- mac-x86-64
|
||||
- mac-arm64
|
||||
- windows-x86-64
|
||||
include:
|
||||
- name: linux-amd64-gnu
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
cross: false
|
||||
experimental: false
|
||||
|
||||
- name: linux-amd64-musl
|
||||
os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: linux-i686-musl
|
||||
os: ubuntu-latest
|
||||
target: i686-unknown-linux-musl
|
||||
cross: true
|
||||
experimental: true
|
||||
|
||||
- name: linux-armhf-gnu
|
||||
os: ubuntu-latest
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: linux-arm64-gnu
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-gnu
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: linux-arm64-musl
|
||||
os: ubuntu-latest
|
||||
target: aarch64-unknown-linux-musl
|
||||
cross: true
|
||||
experimental: true
|
||||
|
||||
- name: linux-s390x-gnu
|
||||
os: ubuntu-latest
|
||||
target: s390x-unknown-linux-gnu
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: linux-ppc64le-gnu
|
||||
os: ubuntu-latest
|
||||
target: powerpc64le-unknown-linux-gnu
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: mac-x86-64
|
||||
os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
cross: false
|
||||
experimental: false
|
||||
|
||||
- name: mac-arm64
|
||||
os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
cross: true
|
||||
experimental: false
|
||||
|
||||
- name: windows-x86-64
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
cross: false
|
||||
experimental: false
|
||||
|
||||
#- name: windows-arm64
|
||||
# os: windows-latest
|
||||
# target: aarch64-pc-windows-msvc
|
||||
# cross: true
|
||||
# experimental: true
|
||||
|
||||
name: Binaries for ${{ matrix.name }}
|
||||
needs: info
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
|
||||
env:
|
||||
version: ${{ needs.info.outputs.cli_version }}
|
||||
dst: watchexec-${{ needs.info.outputs.cli_version }}-${{ matrix.target }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# https://github.com/actions/cache/issues/752
|
||||
- if: ${{ runner.os == 'Windows' }}
|
||||
name: Use GNU tar
|
||||
shell: cmd
|
||||
run: |
|
||||
echo "Adding GNU tar to PATH"
|
||||
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
|
||||
|
||||
- name: Configure caching
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-${{ matrix.target }}-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- run: sudo apt update
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
- name: Add musl tools
|
||||
run: sudo apt install -y musl musl-dev musl-tools
|
||||
if: endsWith(matrix.target, '-musl')
|
||||
- name: Add aarch-gnu tools
|
||||
run: sudo apt install -y gcc-aarch64-linux-gnu
|
||||
if: startsWith(matrix.target, 'aarch64-unknown-linux')
|
||||
- name: Add arm7hf-gnu tools
|
||||
run: sudo apt install -y gcc-arm-linux-gnueabihf
|
||||
if: startsWith(matrix.target, 'armv7-unknown-linux-gnueabihf')
|
||||
- name: Add s390x-gnu tools
|
||||
run: sudo apt install -y gcc-s390x-linux-gnu
|
||||
if: startsWith(matrix.target, 's390x-unknown-linux-gnu')
|
||||
- name: Add ppc64le-gnu tools
|
||||
run: sudo apt install -y gcc-powerpc64le-linux-gnu
|
||||
if: startsWith(matrix.target, 'powerpc64le-unknown-linux-gnu')
|
||||
|
||||
- name: Install cargo-deb
|
||||
if: startsWith(matrix.name, 'linux-')
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-deb
|
||||
|
||||
- name: Install cargo-generate-rpm
|
||||
if: startsWith(matrix.name, 'linux-')
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-generate-rpm
|
||||
|
||||
- name: Configure toolchain
|
||||
run: |
|
||||
rustup toolchain install --profile minimal --no-self-update stable
|
||||
rustup default stable
|
||||
rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Install cross
|
||||
if: matrix.cross
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: |
|
||||
${{ matrix.cross && 'cross' || 'cargo' }} build \
|
||||
-p watchexec-cli \
|
||||
--release --locked \
|
||||
--target ${{ matrix.target }}
|
||||
|
||||
- name: Make manpage
|
||||
shell: bash
|
||||
run: |
|
||||
cargo run -p watchexec-cli \
|
||||
${{ (!matrix.cross) && '--release --target' || '' }} \
|
||||
${{ (!matrix.cross) && matrix.target || '' }} \
|
||||
--locked -- --manual > doc/watchexec.1
|
||||
|
||||
- name: Make completions
|
||||
shell: bash
|
||||
run: |
|
||||
bin/completions \
|
||||
${{ (!matrix.cross) && '--release --target' || '' }} \
|
||||
${{ (!matrix.cross) && matrix.target || '' }} \
|
||||
--locked
|
||||
|
||||
- name: Package
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
ext=""
|
||||
[[ "${{ matrix.name }}" == windows-* ]] && ext=".exe"
|
||||
bin="target/${{ matrix.target }}/release/watchexec${ext}"
|
||||
objcopy --compress-debug-sections "$bin" || true
|
||||
|
||||
mkdir "$dst"
|
||||
|
||||
mkdir -p "target/release"
|
||||
cp "$bin" "target/release/" # workaround for cargo-deb silliness with targets
|
||||
|
||||
cp "$bin" "$dst/"
|
||||
cp -r crates/cli/README.md LICENSE completions doc/{logo.svg,watchexec.1{,.*}} "$dst/"
|
||||
|
||||
- name: Archive (tar)
|
||||
if: '! startsWith(matrix.name, ''windows-'')'
|
||||
run: tar cavf "$dst.tar.xz" "$dst"
|
||||
- name: Archive (deb)
|
||||
if: startsWith(matrix.name, 'linux-')
|
||||
run: cargo deb -p watchexec-cli --no-build --no-strip --target ${{ matrix.target }} --output "$dst.deb"
|
||||
- name: Archive (rpm)
|
||||
if: startsWith(matrix.name, 'linux-')
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
shopt -s globstar
|
||||
cargo generate-rpm -p crates/cli --target "${{ matrix.target }}" --target-dir "target/${{ matrix.target }}"
|
||||
mv target/**/*.rpm "$dst.rpm"
|
||||
- name: Archive (zip)
|
||||
if: startsWith(matrix.name, 'windows-')
|
||||
shell: bash
|
||||
run: 7z a "$dst.zip" "$dst"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.name }}
|
||||
retention-days: 1
|
||||
path: |
|
||||
watchexec-*.tar.xz
|
||||
watchexec-*.tar.zst
|
||||
watchexec-*.deb
|
||||
watchexec-*.rpm
|
||||
watchexec-*.zip
|
||||
|
||||
upload:
|
||||
needs: [build, info]
|
||||
|
||||
name: Checksum and publish
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install b3sum
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: b3sum
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Dist manifest
|
||||
run: |
|
||||
jq -ncf .github/workflows/dist-manifest.jq \
|
||||
--arg version "${{ needs.info.outputs.cli_version }}" \
|
||||
--arg files "$(ls watchexec-*)" \
|
||||
> dist-manifest.json
|
||||
|
||||
- name: Bulk checksums
|
||||
run: |
|
||||
b3sum watchexec-* | tee B3SUMS
|
||||
sha512sum watchexec-* | tee SHA512SUMS
|
||||
sha256sum watchexec-* | tee SHA256SUMS
|
||||
|
||||
- name: File checksums
|
||||
run: |
|
||||
for file in watchexec-*; do
|
||||
b3sum --no-names $file > "$file.b3"
|
||||
sha256sum $file | cut -d ' ' -f1 > "$file.sha256"
|
||||
sha512sum $file | cut -d ' ' -f1 > "$file.sha512"
|
||||
done
|
||||
|
||||
- uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564
|
||||
with:
|
||||
tag_name: v${{ needs.info.outputs.cli_version }}
|
||||
name: CLI v${{ needs.info.outputs.cli_version }}
|
||||
append_body: true
|
||||
files: |
|
||||
dist-manifest.json
|
||||
watchexec-*.tar.xz
|
||||
watchexec-*.tar.zst
|
||||
watchexec-*.deb
|
||||
watchexec-*.rpm
|
||||
watchexec-*.zip
|
||||
*SUMS
|
||||
*.b3
|
||||
*.sha*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
148
.github/workflows/tests.yml
vendored
Normal file
148
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,148 @@
|
|||
name: Test suite
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags-ignore:
|
||||
- "*"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- macos
|
||||
- ubuntu
|
||||
- windows
|
||||
|
||||
name: Test ${{ matrix.platform }}
|
||||
runs-on: "${{ matrix.platform }}-latest"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure toolchain
|
||||
run: |
|
||||
rustup toolchain install --profile minimal --no-self-update stable
|
||||
rustup default stable
|
||||
|
||||
# https://github.com/actions/cache/issues/752
|
||||
- if: ${{ runner.os == 'Windows' }}
|
||||
name: Use GNU tar
|
||||
shell: cmd
|
||||
run: |
|
||||
echo "Adding GNU tar to PATH"
|
||||
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
|
||||
|
||||
- name: Cargo caching
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-stable-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Compilation caching
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: target/
|
||||
key: ${{ runner.os }}-target-stable-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run test suite
|
||||
run: cargo test
|
||||
- name: Run watchexec-events integration tests
|
||||
run: cargo test -p watchexec-events -F serde
|
||||
- name: Check that CLI runs
|
||||
run: cargo run -p watchexec-cli -- -1 echo
|
||||
|
||||
- name: Install coreutils on mac
|
||||
if: ${{ matrix.platform == 'macos' }}
|
||||
run: brew install coreutils
|
||||
|
||||
- name: Run watchexec integration tests (unix)
|
||||
if: ${{ matrix.platform != 'windows' }}
|
||||
run: crates/cli/run-tests.sh
|
||||
shell: bash
|
||||
env:
|
||||
WATCHEXEC_BIN: target/debug/watchexec
|
||||
|
||||
- name: Run bosion integration tests
|
||||
run: ./run-tests.sh
|
||||
working-directory: crates/bosion
|
||||
shell: bash
|
||||
|
||||
- name: Generate manpage
|
||||
run: cargo run -p watchexec-cli -- --manual > doc/watchexec.1
|
||||
- name: Check that manpage is up to date
|
||||
run: git diff --exit-code -- doc/
|
||||
|
||||
- name: Generate completions
|
||||
run: bin/completions
|
||||
- name: Check that completions are up to date
|
||||
run: git diff --exit-code -- completions/
|
||||
|
||||
cross-checks:
|
||||
name: Checks only against select targets
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure toolchain
|
||||
run: |
|
||||
rustup toolchain install --profile minimal --no-self-update stable
|
||||
rustup default stable
|
||||
|
||||
sudo apt-get install -y musl-tools
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
- name: Install cross
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cross
|
||||
|
||||
- name: Cargo caching
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: ${{ runner.os }}-cargo-stable-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-stable-
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- run: cargo check --target x86_64-unknown-linux-musl
|
||||
- run: cross check --target x86_64-unknown-freebsd
|
||||
- run: cross check --target x86_64-unknown-netbsd
|
||||
|
||||
tests-pass:
|
||||
if: always()
|
||||
name: Tests pass
|
||||
needs:
|
||||
- test
|
||||
- cross-checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,3 @@
|
|||
target
|
||||
*.tar.gz
|
||||
*.zip
|
||||
/watchexec-*
|
||||
watchexec.*.log
|
||||
|
|
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
hard_tabs = true
|
83
.travis.yml
83
.travis.yml
|
@ -1,83 +0,0 @@
|
|||
dist: xenial
|
||||
language: rust
|
||||
|
||||
cache:
|
||||
- directories:
|
||||
- $HOME/.cargo
|
||||
- target/debug/deps
|
||||
- target/$TARGET/debug/deps
|
||||
before_cache:
|
||||
# Travis can't cache files that are not readable by "others"
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- fakeroot
|
||||
- musl-dev
|
||||
- musl-tools
|
||||
|
||||
env:
|
||||
global:
|
||||
- PROJECT_NAME=watchexec
|
||||
|
||||
jobs:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- env: CARGO_CLIPPY=1 TARGET=x86_64-unknown-linux-gnu
|
||||
- env: TARGET=i686-unknown-linux-musl
|
||||
- env: TARGET=x86_64-unknown-linux-musl
|
||||
- env: TARGET=x86_64-pc-windows-gnu
|
||||
include:
|
||||
# Default test+release versions
|
||||
- os: windows
|
||||
rust: stable
|
||||
env: TARGET=x86_64-pc-windows-msvc
|
||||
- os: osx
|
||||
rust: stable
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
# Extra targets, released on best-effort
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=i686-unknown-linux-musl
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
- os: windows
|
||||
rust: stable
|
||||
env: TARGET=x86_64-pc-windows-gnu
|
||||
|
||||
# Minimum version advertised in readme
|
||||
- os: windows
|
||||
rust: 1.38.0
|
||||
env: TARGET=x86_64-pc-windows-msvc
|
||||
- os: linux
|
||||
rust: 1.38.0
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
# Clippy only
|
||||
- env: CARGO_CLIPPY=1 TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
before_install: ci/before.sh
|
||||
script: ci/script.sh
|
||||
|
||||
before_deploy: ci/deploy.sh
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: sbV2K4G2SA78U6d8SNZKExenWnuv1MsJ/9ovDDH4ucnzkpJEWDV2iwPklcu5oaRj3Sz4jYuYXToqcbJmSjL5eXjlk8rh2sG2LWT+8Up3X1vxte2XFXko15I/613rD8E/qWfS9FqzmuhMX+gb4P7OwWvVUwtw0IIuSGfBW/TEgUTFUZnUmgdm5ra8VnV3CvmTbn8botxbkdAUvk4C0g7yqHjlV7v9xU+DEXz2Y820cAH8ulu1ZU3JBm+XfVzZ09kByeQ7wnvyRuE4RhVtKK8nKUy+2JF7HX5N+0Du8z9ZHosV6+uoUz9i2OecYzAvL8xKiSkeHBqTxIDTeM4lnnDmnm5LsJ4aEU6pBSuWhglmflTbtAN7rBfYgZGJ6je6Gem5bOcCDtGI7+2qjf00Jo7vbmyK6D6Y6yxwf3W0QnOZcXrn9BWZLMMgochIBlVTTM1zFodcprpdHo8iHNVms3A++WqLnp1O0L/55id59VITGJNafy2vmXU/nlQi2MO03s3SF3jdHT7rchYjJRAcGR79QtCLiL3CbYnaQJsDNviyMm1VC6hkst0tXB8t12v2ht5NU7NEN8E31jnnRLRnwr7LUFRgOzFVF0M5jSqs3eCLnYyI7gCMKL2qOZ2yxJuD9bKsVZDpVvUqnaj5ifE+TMYoONPrc9W1hTyfcND9MhCsM+g=
|
||||
file_glob: true
|
||||
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.*
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: watchexec/watchexec
|
||||
tags: true
|
||||
rust: stable
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
17
CITATION.cff
Normal file
17
CITATION.cff
Normal file
|
@ -0,0 +1,17 @@
|
|||
cff-version: 1.2.0
|
||||
message: |
|
||||
If you use this software, please cite it using these metadata.
|
||||
title: "Watchexec: a tool to react to filesystem changes, and a crate ecosystem to power it"
|
||||
|
||||
version: "2.2.0"
|
||||
date-released: 2024-10-14
|
||||
|
||||
repository-code: https://github.com/watchexec/watchexec
|
||||
license: Apache-2.0
|
||||
|
||||
authors:
|
||||
- family-names: Green
|
||||
given-names: Matt
|
||||
- family-names: Saparelli
|
||||
given-names: Félix
|
||||
orcid: https://orcid.org/0000-0002-2010-630X
|
123
CONTRIBUTING.md
123
CONTRIBUTING.md
|
@ -1,29 +1,14 @@
|
|||
# Contribution guidebook
|
||||
|
||||
|
||||
This is a fairly free-form project, with low contribution traffic. It is mostly passively
|
||||
maintained, with occasional bouts of more active developing, usually along a theme. Most of the
|
||||
maintainer activity is triaging and managing issues, and loosely planning/grouping feature requests
|
||||
and non-critical bugs to fix into these themes, represented as milestones.
|
||||
This is a fairly free-form project, with low contribution traffic.
|
||||
|
||||
Maintainers:
|
||||
|
||||
- Félix Saparelli (@passcod) (active)
|
||||
- Matt Green (@mattgreen) (original author, more passive now)
|
||||
- Matt Green (@mattgreen) (original author, mostly checked out)
|
||||
|
||||
Currently the project is in a long v1 phase, with a "sometime in the future" second phase, which at
|
||||
the moment I envision as something with a stronger semver focus, and a major version number that may
|
||||
increase more often. Possibly the project would be split in two or more crates, to facilitate this.
|
||||
|
||||
So, semver. Watchexec remains semver-compatible... on the CLI interface only. The library interface
|
||||
is explicitly _not_ considered as part of breaking changes, and even _patch_ version number changes
|
||||
may actually involve API breakage at the library interface.
|
||||
|
||||
Additionally, it is to be considered unwise to rely on the _output_ of the CLI as an interface. The
|
||||
semver aspects are fairly loose but focus on input (flags, options, arguments, env), behaviour, and
|
||||
the calling environment of the sub process.
|
||||
|
||||
Contributions are accepted for literally everything. There are a few anti goals:
|
||||
There are a few anti goals:
|
||||
|
||||
- Calling watchexec is to be a **simple** exercise that remains intuitive. As a specific point, it
|
||||
should not involve any piping or require xargs.
|
||||
|
@ -33,6 +18,20 @@ Contributions are accepted for literally everything. There are a few anti goals:
|
|||
but watchexec itself will remain generic, usable for any purpose.
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
To enable verbose logging in tests, run with:
|
||||
|
||||
```console
|
||||
$ env RUST_LOG=watchexec=trace,info RUST_TEST_THREADS=1 RUST_NOCAPTURE=1 cargo test --test testfile -- testname
|
||||
```
|
||||
|
||||
To use [Tokio Console](https://github.com/tokio-rs/console):
|
||||
|
||||
1. Add `--cfg tokio_unstable` to your `RUSTFLAGS`.
|
||||
2. Run the CLI with the `dev-console` feature.
|
||||
|
||||
|
||||
## PR etiquette
|
||||
|
||||
- Maintainers are busy or may not have the bandwidth, be patient.
|
||||
|
@ -45,57 +44,79 @@ Apart from that, welcome and thank you for your time!
|
|||
|
||||
## Releasing
|
||||
|
||||
A release goes through these steps:
|
||||
```
|
||||
cargo release -p crate-name --execute patch # or minor, major
|
||||
```
|
||||
|
||||
0. Versioning indications on PRs. These are statements by the maintainers when reviewing or
|
||||
discussing PRs generally in `[square brackets]` that make an indication of what implication
|
||||
merging the PR will have on versioning. For example:
|
||||
When a CLI release is done, the [release notes](https://github.com/watchexec/watchexec/releases) should be edited with the changelog.
|
||||
|
||||
> [versioning note: this is a feature add but not considered an api breaking change (due to the builder)]
|
||||
### Release order
|
||||
|
||||
1. Opening a draft release. Before even merging anything, a draft (only visible privately) release
|
||||
is made. These are a github feature and only visible to maintainers. Name the release... that can
|
||||
be descriptive or whimsical or both. Release title template is `{version without v}: {title}`.
|
||||
Use this command to see the tree of workspace dependencies:
|
||||
|
||||
2. Adding each change to the draft release. The releases pages on github serves as a changelog, so
|
||||
this is worth getting right. One sentence per change, focusing on what it is, what it adds, what
|
||||
it changes, if any. Add a link or PR/issue number if relevant. For example:
|
||||
```console
|
||||
$ cargo tree -p watchexec-cli | rg -F '(/' --color=never | sed 's/ v[0-9].*//'
|
||||
```
|
||||
|
||||
> - #160 :warning: Stop initialising the logger in the library code. Downstream users will need
|
||||
> to initialise their own logger if they want debug/warn output.
|
||||
## Overview
|
||||
|
||||
3. Merging the PRs. Merge commits are preferred over rebase or squash.
|
||||
The architecture of watchexec is roughly:
|
||||
|
||||
4. Cleaning up the code and documentation if needed. For example a PR that adds a flag may not have
|
||||
also added the corresponding completions, manpage entries, readme entries. Or two PRs may
|
||||
conflict slightly or do the same thing twice, in which case harmonising things is required here.
|
||||
- sources gather events
|
||||
- events are debounced and filtered
|
||||
- event(s) make it through the debounce/filters and trigger an "action"
|
||||
- `on_action` handler is called, returning an `Outcome`
|
||||
- outcome is processed into managing the command that watchexec is running
|
||||
- outcome can also be to exit
|
||||
- when a command is started, the `on_pre_spawn` and `on_post_spawn` handlers are called
|
||||
- commands are also a source of events, so e.g. "command has finished" is handled by `on_action`
|
||||
|
||||
5. Run `cargo fmt`, `cargo test`, `cargo clippy`. CI will also run, wait for that. In the meantime:
|
||||
And this is the startup sequence:
|
||||
- init config sets basic immutable facts about the runtime
|
||||
- runtime starts:
|
||||
- source workers start, and are passed their runtime config
|
||||
- action worker starts, and is passed its runtime config
|
||||
- (unless `--postpone` is given) a synthetic event is injected to kickstart things
|
||||
|
||||
6. Run through related issues to the PRs and close them if that wasn't done automatically. Or if the
|
||||
PRs only fixed a problem partially, chime in to mention that, and to restate what remains to fix.
|
||||
## Guides
|
||||
|
||||
7. "Real" test the new code. If new options were added, test those.
|
||||
These are generic guides for implementing specific bits of functionality.
|
||||
|
||||
8. Check for any dependency updates with `cargo outdated -R`.
|
||||
### Adding an event source
|
||||
|
||||
9. Change the version number, run `cargo check`, and make a commit that contains _only_ the
|
||||
Cargo.toml and Cargo.lock changes. Use the version number (without `v` prefix) as only message.
|
||||
- add a worker for "sourcing" events. Looking at the [signal source
|
||||
worker](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/signal/source.rs) is
|
||||
probably easiest to get started here.
|
||||
|
||||
10. Create an annotated tag named the naked version (without `v` prefix), with identical message:
|
||||
- because we may not always want to enable this event source, and just to be flexible, add [runtime
|
||||
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) for the source.
|
||||
|
||||
```
|
||||
$ git tag -am 1.14.0{,}
|
||||
```
|
||||
- for convenience, probably add [a method on the runtime
|
||||
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) which
|
||||
configures the most common usecase.
|
||||
|
||||
11. Push: `$ git push --follow-tags`.
|
||||
- because watchexec is reconfigurable, in the worker you'll need to react to config changes. Look at
|
||||
how the [fs worker does it](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/fs.rs)
|
||||
for reference.
|
||||
|
||||
12. Edit the draft release, select the newly pushed tag, and publish it. Built artifacts will get
|
||||
built and get added automatically to the release as downloads.
|
||||
- you may need to [add to the event tag
|
||||
enum](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/event.rs).
|
||||
|
||||
13. Run the `cargo publish`.
|
||||
- if you do, you should [add support to the "tagged
|
||||
filterer"](https://github.com/watchexec/watchexec/blob/main/crates/filterer/tagged/src/parse.rs),
|
||||
but this can be done in follow-up work.
|
||||
|
||||
14. Announce the release.
|
||||
### Process a new event in the CLI
|
||||
|
||||
- add an option to the
|
||||
[args](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/args.rs) if necessary
|
||||
|
||||
- add to the [runtime
|
||||
config](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs) when
|
||||
the option is present
|
||||
|
||||
- process relevant events [in the action
|
||||
handler](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs)
|
||||
|
||||
---
|
||||
vim: tw=100
|
||||
|
|
4627
Cargo.lock
generated
4627
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
86
Cargo.toml
86
Cargo.toml
|
@ -1,55 +1,43 @@
|
|||
[package]
|
||||
name = "watchexec"
|
||||
version = "1.14.0"
|
||||
authors = ["Matt Green <mattgreenrocks@gmail.com>"]
|
||||
description = "Executes commands in response to file modifications"
|
||||
documentation = "https://github.com/watchexec/watchexec"
|
||||
homepage = "https://github.com/watchexec/watchexec"
|
||||
repository = "https://github.com/watchexec/watchexec"
|
||||
readme = "README.md"
|
||||
keywords = ["watcher", "inotify", "fsevents", "kqueue"]
|
||||
categories = ["command-line-utilities"]
|
||||
license = "Apache-2.0"
|
||||
exclude = ["/ci/*", "/pkg/*", "/.travis.yml", "/Makefile", "/appveyor.yml"]
|
||||
edition = "2018"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/lib",
|
||||
"crates/cli",
|
||||
"crates/events",
|
||||
"crates/signals",
|
||||
"crates/supervisor",
|
||||
"crates/filterer/globset",
|
||||
"crates/filterer/ignore",
|
||||
"crates/bosion",
|
||||
"crates/ignore-files",
|
||||
"crates/project-origins",
|
||||
]
|
||||
|
||||
[badges]
|
||||
appveyor = { repository = "watchexec/watchexec" }
|
||||
travis-ci = { repository = "watchexec/watchexec" }
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
[workspace.dependencies]
|
||||
miette = "5.10.0"
|
||||
tempfile = "3.8.0"
|
||||
tracing-test = "0.2.4"
|
||||
rand = "0.8"
|
||||
uuid = "1.5.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
debug = 1 # for stack traces
|
||||
codegen-units = 1
|
||||
strip = "symbols"
|
||||
|
||||
[dependencies]
|
||||
glob = "0.3.0"
|
||||
globset = "0.4.5"
|
||||
lazy_static = "1.1.0"
|
||||
log = "0.4.4"
|
||||
notify = "4.0.15"
|
||||
derive_builder = "0.9.0"
|
||||
walkdir = "2.3.1"
|
||||
[profile.dev.build-override]
|
||||
opt-level = 0
|
||||
codegen-units = 1024
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
incremental = false
|
||||
|
||||
[dependencies.clap]
|
||||
version = "2.33.1"
|
||||
default-features = false
|
||||
features = ["wrap_help"]
|
||||
|
||||
[dependencies.env_logger]
|
||||
version = "0.7.1"
|
||||
default-features = false
|
||||
features = []
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = "0.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi]
|
||||
version = "0.3.9"
|
||||
features = ["ioapiset", "jobapi2", "tlhelp32"]
|
||||
|
||||
[[bin]]
|
||||
name = "watchexec"
|
||||
doc = false
|
||||
[profile.release.build-override]
|
||||
opt-level = 0
|
||||
codegen-units = 1024
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
overflow-checks = false
|
||||
incremental = false
|
||||
|
|
30
Makefile
30
Makefile
|
@ -1,30 +0,0 @@
|
|||
LATEST_TAG=$(shell git tag | tail -n1)
|
||||
|
||||
.PHONY: doc test
|
||||
|
||||
debug: src/* Cargo.toml
|
||||
@cargo build
|
||||
|
||||
release: src/* Cargo.toml
|
||||
@cargo build --release --locked
|
||||
|
||||
clean:
|
||||
@cargo clean
|
||||
|
||||
test:
|
||||
@cargo test
|
||||
|
||||
doc: doc/watchexec.1.ronn
|
||||
@ronn doc/watchexec.1.ronn
|
||||
|
||||
cargo-release:
|
||||
@cargo publish
|
||||
|
||||
homebrew-release:
|
||||
@brew bump-formula-pr --strict --url="https://github.com/mattgreen/watchexec/archive/$(LATEST_TAG).tar.gz" watchexec
|
||||
|
||||
install: release
|
||||
@cp target/release/watchexec /usr/bin
|
||||
|
||||
uninstall:
|
||||
@rm /usr/bin/watchexec
|
143
README.md
143
README.md
|
@ -1,131 +1,84 @@
|
|||
# watchexec
|
||||
[![CI status on main branch](https://github.com/watchexec/watchexec/actions/workflows/tests.yml/badge.svg)](https://github.com/watchexec/watchexec/actions/workflows/tests.yml)
|
||||
|
||||
[![Build status](https://badgen.net/travis/watchexec/watchexec/main)](https://travis-ci.org/watchexec/watchexec)
|
||||
[![Crates.io status](https://badgen.net/crates/v/watchexec)](https://crates.io/crates/watchexec)
|
||||
[![Docs status](https://docs.rs/watchexec/badge.svg)](https://docs.rs/watchexec)
|
||||
# Watchexec
|
||||
|
||||
Software development often involves running the same commands over and over. Boring!
|
||||
|
||||
`watchexec` is a **simple**, standalone tool that watches a path and runs a command whenever it detects modifications.
|
||||
`watchexec` is a simple, standalone tool that watches a path and runs a command whenever it detects modifications.
|
||||
|
||||
Example use cases:
|
||||
|
||||
* Automatically run unit tests
|
||||
* Run linters/syntax checkers
|
||||
* Rebuild artifacts
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Simple invocation and use
|
||||
* Simple invocation and use, does not require a cryptic command line involving `xargs`
|
||||
* Runs on OS X, Linux, and Windows
|
||||
* Monitors current directory and all subdirectories for changes
|
||||
* Uses most efficient event polling mechanism for your platform (except for [BSD](https://github.com/notify-rs/notify#todo))
|
||||
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
|
||||
* By default, uses `.gitignore` and `.ignore` to determine which files to ignore notifications for
|
||||
* Support for watching files with a specific extension
|
||||
* Support for filtering/ignoring events based on [glob patterns](https://docs.rs/globset/*/globset/#syntax)
|
||||
* Launches child processes in a new process group
|
||||
* Sets the following environment variables in the child process:
|
||||
* If a single file changed (depending on the event type):
|
||||
* `$WATCHEXEC_CREATED_PATH`, the path of the file that was created
|
||||
* `$WATCHEXEC_REMOVED_PATH`, the path of the file that was removed
|
||||
* `$WATCHEXEC_RENAMED_PATH`, the path of the file that was renamed
|
||||
* `$WATCHEXEC_WRITTEN_PATH`, the path of the file that was modified
|
||||
* `$WATCHEXEC_META_CHANGED_PATH`, the path of the file whose metadata changed
|
||||
* If multiple files changed:
|
||||
* `$WATCHEXEC_COMMON_PATH`, the longest common path of all of the files that triggered a change
|
||||
* This can be disabled or limited with `--no-environment` and `--no-meta`
|
||||
* Optionally clears screen between executions
|
||||
* Optionally restarts the command with every modification (good for servers)
|
||||
* Does not require a language runtime
|
||||
* Loads `.gitignore` and `.ignore` files
|
||||
* Uses process groups to keep hold of forking programs
|
||||
* Provides the paths that changed in environment variables or STDIN
|
||||
* Does not require a language runtime, not tied to any particular language or ecosystem
|
||||
* [And more!](./crates/cli/#features)
|
||||
|
||||
## Anti-Features
|
||||
|
||||
* Not tied to any particular language or ecosystem
|
||||
* Does not require a cryptic command line involving `xargs`
|
||||
## Quick start
|
||||
|
||||
## Usage Examples
|
||||
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `npm run build` when a change is detected:
|
||||
|
||||
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `make` when a change is detected:
|
||||
|
||||
$ watchexec --exts js,css,html make
|
||||
|
||||
Call `make test` when any file changes in this directory/subdirectory, except for everything below `target`:
|
||||
|
||||
$ watchexec -i target make test
|
||||
|
||||
Call `ls -la` when any file changes in this directory/subdirectory:
|
||||
|
||||
$ watchexec -- ls -la
|
||||
$ watchexec -e js,css,html npm run build
|
||||
|
||||
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
|
||||
|
||||
$ watchexec -e py -r python server.py
|
||||
$ watchexec -r -e py -- python server.py
|
||||
|
||||
Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the child process:
|
||||
More usage examples: [in the CLI README](./crates/cli/#usage-examples)!
|
||||
|
||||
$ watchexec -r -s SIGKILL my_server
|
||||
## Install
|
||||
|
||||
Send a SIGHUP to the child process upon changes (Note: with using `-n | --no-shell` here, we're executing `my_server` directly, instead of wrapping it in a shell:
|
||||
<a href="https://repology.org/project/watchexec/versions"><img align="right" src="https://repology.org/badge/vertical-allrepos/watchexec.svg" alt="Packaging status"></a>
|
||||
|
||||
$ watchexec -n -s SIGHUP my_server
|
||||
- With [your package manager](./doc/packages.md) for Arch, Debian, Homebrew, Nix, Scoop, Chocolatey…
|
||||
- From binary with [Binstall](https://github.com/cargo-bins/cargo-binstall): `cargo binstall watchexec-cli`
|
||||
- As [pre-built binary package from Github](https://github.com/watchexec/watchexec/releases/latest)
|
||||
- From source with Cargo: `cargo install --locked watchexec-cli`
|
||||
|
||||
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
|
||||
All options in detail: [in the CLI README](./crates/cli/#installation),
|
||||
in the online help (`watchexec -h`, `watchexec --help`, or `watchexec --manual`),
|
||||
and [in the manual page](./doc/watchexec.1.md).
|
||||
|
||||
$ watchexec make
|
||||
|
||||
Run `make` when any file in `lib` or `src` changes:
|
||||
## Augment
|
||||
|
||||
$ watchexec -w lib -w src make
|
||||
Watchexec pairs well with:
|
||||
|
||||
## Installation
|
||||
- [checkexec](https://github.com/kurtbuilds/checkexec): to run only when source files are newer than a target file
|
||||
- [just](https://github.com/casey/just): a modern alternative to `make`
|
||||
- [systemfd](https://github.com/mitsuhiko/systemfd): socket-passing in development
|
||||
|
||||
### Cargo
|
||||
## Extend
|
||||
|
||||
watchexec requires Rust 1.38 or later. You can install it using cargo:
|
||||
- [watchexec library](./crates/lib/): to create more specialised watchexec-powered tools.
|
||||
- [watchexec-events](./crates/events/): event types for watchexec.
|
||||
- [watchexec-signals](./crates/signals/): signal types for watchexec.
|
||||
- [watchexec-supervisor](./crates/supervisor/): process lifecycle manager (the _exec_ part of watchexec).
|
||||
- [clearscreen](https://github.com/watchexec/clearscreen): to clear the (terminal) screen on every platform.
|
||||
- [command group](https://github.com/watchexec/command-group): to run commands in process groups.
|
||||
- [ignore files](./crates/ignore-files/): to find, parse, and interpret ignore files.
|
||||
- [project origins](./crates/project-origins/): to find the origin(s) directory of a project.
|
||||
- [notify](https://github.com/notify-rs/notify): to respond to file modifications (third-party).
|
||||
|
||||
$ cargo install watchexec
|
||||
### Downstreams
|
||||
|
||||
### OS X with Homebrew
|
||||
Selected downstreams of watchexec and associated crates:
|
||||
|
||||
$ brew install watchexec
|
||||
|
||||
### Linux
|
||||
|
||||
For now, use the GitHub Releases tab to obtain the binary. PRs for packaging in unsupported distros are welcomed.
|
||||
|
||||
#### Debian
|
||||
|
||||
A deb package is available for amd64 architectures in the GitHub Releases.
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
Available in the **community** repository:
|
||||
|
||||
$ pacman -S watchexec
|
||||
|
||||
### Windows
|
||||
|
||||
Available [using scoop](https://scoop.sh/):
|
||||
|
||||
#> scoop install watchexec
|
||||
|
||||
Or [chocolatey](https://chocolatey.org/packages/watchexec):
|
||||
|
||||
#> choco install watchexec
|
||||
|
||||
Or just unzip the binary from the GitHub Releases.
|
||||
|
||||
## Shell completions
|
||||
|
||||
Currently available shell completions:
|
||||
|
||||
- zsh: `completions/zsh` should be installed to `/usr/share/zsh/site-functions/_watchexec`
|
||||
|
||||
## Building
|
||||
|
||||
Rust 1.38 or later is required.
|
||||
|
||||
## Credits
|
||||
|
||||
* [notify](https://github.com/passcod/notify) for doing most of the heavy-lifting
|
||||
* [globset](https://crates.io/crates/globset) for super-fast glob matching
|
||||
- [cargo watch](https://github.com/watchexec/cargo-watch): a specialised watcher for Rust/Cargo projects.
|
||||
- [cargo lambda](https://github.com/cargo-lambda/cargo-lambda): a dev tool for Rust-powered AWS Lambda functions.
|
||||
- [create-rust-app](https://create-rust-app.dev): a template for Rust+React web apps.
|
||||
- [dotter](https://github.com/supercuber/dotter): a dotfile manager.
|
||||
- [ghciwatch](https://github.com/mercurytechnologies/ghciwatch): a specialised watcher for Haskell projects.
|
||||
- [tectonic](https://tectonic-typesetting.github.io/book/latest/): a TeX/LaTeX typesetting system.
|
||||
|
|
7
bin/completions
Executable file
7
bin/completions
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
cargo run -p watchexec-cli $* -- --completions bash > completions/bash
|
||||
cargo run -p watchexec-cli $* -- --completions elvish > completions/elvish
|
||||
cargo run -p watchexec-cli $* -- --completions fish > completions/fish
|
||||
cargo run -p watchexec-cli $* -- --completions nu > completions/nu
|
||||
cargo run -p watchexec-cli $* -- --completions powershell > completions/powershell
|
||||
cargo run -p watchexec-cli $* -- --completions zsh > completions/zsh
|
10
bin/dates.mjs
Executable file
10
bin/dates.mjs
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const id = Math.floor(Math.random() * 100);
|
||||
let n = 0;
|
||||
const m = 5;
|
||||
while (n < m) {
|
||||
n += 1;
|
||||
console.log(`[${id} : ${n}/${m}] ${new Date}`);
|
||||
await new Promise(done => setTimeout(done, 2000));
|
||||
}
|
3
bin/manpage
Executable file
3
bin/manpage
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
cargo run -p watchexec-cli -- --manual > doc/watchexec.1
|
||||
pandoc doc/watchexec.1 -t markdown > doc/watchexec.1.md
|
18
ci/before.sh
18
ci/before.sh
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
rustup target add $TARGET
|
||||
cargo clean --target $TARGET --verbose
|
||||
|
||||
if [ $TRAVIS_OS_NAME = windows ]; then
|
||||
choco install windows-sdk-10.1
|
||||
fi
|
||||
|
||||
if [[ ! -z "$CARGO_AUDIT" ]]; then
|
||||
which cargo-audit || cargo install --debug cargo-audit
|
||||
# --debug for faster build at the minimal expense of runtime speed
|
||||
elif [[ ! -z "$CARGO_CLIPPY" ]]; then
|
||||
rustup component add clippy
|
||||
fi
|
||||
|
87
ci/deploy.sh
87
ci/deploy.sh
|
@ -1,87 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ ! -z "$CARGO_CLIPPY" ]]; then
|
||||
echo Clippy says: I shan’t deploy
|
||||
exit 1
|
||||
fi
|
||||
|
||||
### Vars
|
||||
|
||||
project="$PROJECT_NAME"
|
||||
tag="$TRAVIS_TAG"
|
||||
target="$TARGET"
|
||||
|
||||
[[ -z "$project" ]] && exit 21
|
||||
[[ -z "$tag" ]] && exit 22
|
||||
[[ -z "$target" ]] && exit 23
|
||||
|
||||
ext=""
|
||||
windows=""
|
||||
if [[ "$target" == *"windows"* ]]; then
|
||||
choco install 7zip
|
||||
ext=".exe"
|
||||
windows="1"
|
||||
fi
|
||||
|
||||
build_dir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
out_dir=$(pwd)
|
||||
name="$project-$tag-$target"
|
||||
|
||||
### Build
|
||||
|
||||
cargo build --target $target --release --locked
|
||||
|
||||
### Decorate
|
||||
|
||||
mkdir "$build_dir/$name"
|
||||
cp -v "target/$target/release/$project$ext" "$build_dir/$name/"
|
||||
cp -v LICENSE "$build_dir/$name/"
|
||||
cp -v README.md "$build_dir/$name/"
|
||||
cp -v completions/zsh "$build_dir/$name/"
|
||||
cp -v doc/watchexec.1 "$build_dir/$name/"
|
||||
ls -shal "$build_dir/$name/"
|
||||
|
||||
### Strip
|
||||
|
||||
cd "$build_dir"
|
||||
strip "$name/$project$ext"
|
||||
ls -shal "$name/"
|
||||
|
||||
### Pack
|
||||
|
||||
if [[ -z "$windows" ]]; then
|
||||
tar cvf "$out_dir/$name.tar" "$name"
|
||||
cd "$out_dir"
|
||||
xz -f9 "$name.tar"
|
||||
else
|
||||
7z a "$out_dir/$name.zip" "$name"
|
||||
fi
|
||||
|
||||
### Debify
|
||||
|
||||
if [[ "$target" == "x86_64-unknown-linux-gnu" ]]; then
|
||||
mkdir -p "$build_dir/deb/$name"
|
||||
cd "$build_dir/deb/$name"
|
||||
|
||||
mkdir -p DEBIAN usr/bin usr/share/man/man1 usr/share/zsh/site-functions
|
||||
cp "../../$name/watchexec" usr/bin/
|
||||
cp "../../$name/watchexec.1" usr/share/man/man1/
|
||||
cp "../../$name/zsh" usr/share/zsh/site-functions/_watchexec
|
||||
cat <<CONTROL > DEBIAN/control
|
||||
Package: watchexec
|
||||
Version: ${TRAVIS_TAG}
|
||||
Architecture: amd64
|
||||
Maintainer: Félix Saparelli <aur@passcod.name>
|
||||
Installed-Size: $(du -d1 usr | tail -n1 | cut -d\t -f1)
|
||||
Homepage: https://github.com/watchexec/watchexec
|
||||
Description: Executes commands in response to file modifications.
|
||||
Software development often involves running the same commands over and over. Boring!
|
||||
Watchexec is a simple, standalone tool that watches a path and runs a command whenever it detects modifications.
|
||||
CONTROL
|
||||
cd ..
|
||||
fakeroot dpkg -b "$name"
|
||||
mv "$name.deb" "$out_dir/"
|
||||
popd
|
||||
fi
|
||||
|
||||
ls -shal "$out_dir/"
|
10
ci/script.sh
10
ci/script.sh
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ ! -z "$CARGO_AUDIT" ]]; then
|
||||
cargo check --target $TARGET
|
||||
cargo audit
|
||||
elif [[ ! -z "$CARGO_CLIPPY" ]]; then
|
||||
cargo clippy --target $TARGET
|
||||
else
|
||||
cargo test --target $TARGET
|
||||
fi
|
230
completions/bash
Normal file
230
completions/bash
Normal file
|
@ -0,0 +1,230 @@
|
|||
_watchexec() {
|
||||
local i cur prev opts cmd
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
cmd=""
|
||||
opts=""
|
||||
|
||||
for i in ${COMP_WORDS[@]}
|
||||
do
|
||||
case "${cmd},${i}" in
|
||||
",$1")
|
||||
cmd="watchexec"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "${cmd}" in
|
||||
watchexec)
|
||||
opts="-w -W -F -c -o -r -s -d -p -n -E -1 -N -q -e -f -j -i -v -h -V --watch --watch-non-recursive --watch-file --clear --on-busy-update --restart --signal --stop-signal --stop-timeout --map-signal --debounce --stdin-quit --no-vcs-ignore --no-project-ignore --no-global-ignore --no-default-ignore --no-discover-ignore --ignore-nothing --postpone --delay-run --poll --shell --no-environment --emit-events-to --only-emit-events --env --no-process-group --wrap-process --notify --color --timings --quiet --bell --project-origin --workdir --exts --filter --filter-file --filter-prog --ignore --ignore-file --fs-events --no-meta --print-events --manual --completions --verbose --log-file --help --version [COMMAND]..."
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
fi
|
||||
case "${prev}" in
|
||||
--watch)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-w)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--watch-non-recursive)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-W)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--watch-file)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-F)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--clear)
|
||||
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-c)
|
||||
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--on-busy-update)
|
||||
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--signal)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-s)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--stop-signal)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--stop-timeout)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--map-signal)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--debounce)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-d)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--delay-run)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--poll)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--shell)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--emit-events-to)
|
||||
COMPREPLY=($(compgen -W "environment stdio file json-stdio json-file none" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--env)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-E)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--wrap-process)
|
||||
COMPREPLY=($(compgen -W "group session none" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--project-origin)
|
||||
COMPREPLY=()
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||
compopt -o plusdirs
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
--workdir)
|
||||
COMPREPLY=()
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||
compopt -o plusdirs
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
--exts)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-e)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-f)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter-file)
|
||||
local oldifs
|
||||
if [ -n "${IFS+x}" ]; then
|
||||
oldifs="$IFS"
|
||||
fi
|
||||
IFS=$'\n'
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
if [ -n "${oldifs+x}" ]; then
|
||||
IFS="$oldifs"
|
||||
fi
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||
compopt -o filenames
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
--filter-prog)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-j)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--ignore)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-i)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--ignore-file)
|
||||
local oldifs
|
||||
if [ -n "${IFS+x}" ]; then
|
||||
oldifs="$IFS"
|
||||
fi
|
||||
IFS=$'\n'
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
if [ -n "${oldifs+x}" ]; then
|
||||
IFS="$oldifs"
|
||||
fi
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||
compopt -o filenames
|
||||
fi
|
||||
return 0
|
||||
;;
|
||||
--fs-events)
|
||||
COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--completions)
|
||||
COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--log-file)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=()
|
||||
;;
|
||||
esac
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
|
||||
complete -F _watchexec -o nosort -o bashdefault -o default watchexec
|
||||
else
|
||||
complete -F _watchexec -o bashdefault -o default watchexec
|
||||
fi
|
95
completions/elvish
Normal file
95
completions/elvish
Normal file
|
@ -0,0 +1,95 @@
|
|||
|
||||
use builtin;
|
||||
use str;
|
||||
|
||||
set edit:completion:arg-completer[watchexec] = {|@words|
|
||||
fn spaces {|n|
|
||||
builtin:repeat $n ' ' | str:join ''
|
||||
}
|
||||
fn cand {|text desc|
|
||||
edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
|
||||
}
|
||||
var command = 'watchexec'
|
||||
for word $words[1..-1] {
|
||||
if (str:has-prefix $word '-') {
|
||||
break
|
||||
}
|
||||
set command = $command';'$word
|
||||
}
|
||||
var completions = [
|
||||
&'watchexec'= {
|
||||
cand -w 'Watch a specific file or directory'
|
||||
cand --watch 'Watch a specific file or directory'
|
||||
cand -W 'Watch a specific directory, non-recursively'
|
||||
cand --watch-non-recursive 'Watch a specific directory, non-recursively'
|
||||
cand -F 'Watch files and directories from a file'
|
||||
cand --watch-file 'Watch files and directories from a file'
|
||||
cand -c 'Clear screen before running command'
|
||||
cand --clear 'Clear screen before running command'
|
||||
cand -o 'What to do when receiving events while the command is running'
|
||||
cand --on-busy-update 'What to do when receiving events while the command is running'
|
||||
cand -s 'Send a signal to the process when it''s still running'
|
||||
cand --signal 'Send a signal to the process when it''s still running'
|
||||
cand --stop-signal 'Signal to send to stop the command'
|
||||
cand --stop-timeout 'Time to wait for the command to exit gracefully'
|
||||
cand --map-signal 'Translate signals from the OS to signals to send to the command'
|
||||
cand -d 'Time to wait for new events before taking action'
|
||||
cand --debounce 'Time to wait for new events before taking action'
|
||||
cand --delay-run 'Sleep before running the command'
|
||||
cand --poll 'Poll for filesystem changes'
|
||||
cand --shell 'Use a different shell'
|
||||
cand --emit-events-to 'Configure event emission'
|
||||
cand -E 'Add env vars to the command'
|
||||
cand --env 'Add env vars to the command'
|
||||
cand --wrap-process 'Configure how the process is wrapped'
|
||||
cand --color 'When to use terminal colours'
|
||||
cand --project-origin 'Set the project origin'
|
||||
cand --workdir 'Set the working directory'
|
||||
cand -e 'Filename extensions to filter to'
|
||||
cand --exts 'Filename extensions to filter to'
|
||||
cand -f 'Filename patterns to filter to'
|
||||
cand --filter 'Filename patterns to filter to'
|
||||
cand --filter-file 'Files to load filters from'
|
||||
cand -j '[experimental] Filter programs'
|
||||
cand --filter-prog '[experimental] Filter programs'
|
||||
cand -i 'Filename patterns to filter out'
|
||||
cand --ignore 'Filename patterns to filter out'
|
||||
cand --ignore-file 'Files to load ignores from'
|
||||
cand --fs-events 'Filesystem events to filter to'
|
||||
cand --completions 'Generate a shell completions script'
|
||||
cand --log-file 'Write diagnostic logs to a file'
|
||||
cand -r 'Restart the process if it''s still running'
|
||||
cand --restart 'Restart the process if it''s still running'
|
||||
cand --stdin-quit 'Exit when stdin closes'
|
||||
cand --no-vcs-ignore 'Don''t load gitignores'
|
||||
cand --no-project-ignore 'Don''t load project-local ignores'
|
||||
cand --no-global-ignore 'Don''t load global ignores'
|
||||
cand --no-default-ignore 'Don''t use internal default ignores'
|
||||
cand --no-discover-ignore 'Don''t discover ignore files at all'
|
||||
cand --ignore-nothing 'Don''t ignore anything at all'
|
||||
cand -p 'Wait until first change before running command'
|
||||
cand --postpone 'Wait until first change before running command'
|
||||
cand -n 'Shorthand for ''--shell=none'''
|
||||
cand --no-environment 'Deprecated shorthand for ''--emit-events=none'''
|
||||
cand --only-emit-events 'Only emit events to stdout, run no commands'
|
||||
cand --no-process-group 'Don''t use a process group'
|
||||
cand -1 'Testing only: exit Watchexec after the first run'
|
||||
cand -N 'Alert when commands start and end'
|
||||
cand --notify 'Alert when commands start and end'
|
||||
cand --timings 'Print how long the command took to run'
|
||||
cand -q 'Don''t print starting and stopping messages'
|
||||
cand --quiet 'Don''t print starting and stopping messages'
|
||||
cand --bell 'Ring the terminal bell on command completion'
|
||||
cand --no-meta 'Don''t emit fs events for metadata changes'
|
||||
cand --print-events 'Print events that trigger actions'
|
||||
cand --manual 'Show the manual page'
|
||||
cand -v 'Set diagnostic log level'
|
||||
cand --verbose 'Set diagnostic log level'
|
||||
cand -h 'Print help (see more with ''--help'')'
|
||||
cand --help 'Print help (see more with ''--help'')'
|
||||
cand -V 'Print version'
|
||||
cand --version 'Print version'
|
||||
}
|
||||
]
|
||||
$completions[$command]
|
||||
}
|
52
completions/fish
Normal file
52
completions/fish
Normal file
|
@ -0,0 +1,52 @@
|
|||
complete -c watchexec -s w -l watch -d 'Watch a specific file or directory' -r -F
|
||||
complete -c watchexec -s W -l watch-non-recursive -d 'Watch a specific directory, non-recursively' -r -F
|
||||
complete -c watchexec -s F -l watch-file -d 'Watch files and directories from a file' -r -F
|
||||
complete -c watchexec -s c -l clear -d 'Clear screen before running command' -r -f -a "{clear\t'',reset\t''}"
|
||||
complete -c watchexec -s o -l on-busy-update -d 'What to do when receiving events while the command is running' -r -f -a "{queue\t'',do-nothing\t'',restart\t'',signal\t''}"
|
||||
complete -c watchexec -s s -l signal -d 'Send a signal to the process when it\'s still running' -r
|
||||
complete -c watchexec -l stop-signal -d 'Signal to send to stop the command' -r
|
||||
complete -c watchexec -l stop-timeout -d 'Time to wait for the command to exit gracefully' -r
|
||||
complete -c watchexec -l map-signal -d 'Translate signals from the OS to signals to send to the command' -r
|
||||
complete -c watchexec -s d -l debounce -d 'Time to wait for new events before taking action' -r
|
||||
complete -c watchexec -l delay-run -d 'Sleep before running the command' -r
|
||||
complete -c watchexec -l poll -d 'Poll for filesystem changes' -r
|
||||
complete -c watchexec -l shell -d 'Use a different shell' -r
|
||||
complete -c watchexec -l emit-events-to -d 'Configure event emission' -r -f -a "{environment\t'',stdio\t'',file\t'',json-stdio\t'',json-file\t'',none\t''}"
|
||||
complete -c watchexec -s E -l env -d 'Add env vars to the command' -r
|
||||
complete -c watchexec -l wrap-process -d 'Configure how the process is wrapped' -r -f -a "{group\t'',session\t'',none\t''}"
|
||||
complete -c watchexec -l color -d 'When to use terminal colours' -r -f -a "{auto\t'',always\t'',never\t''}"
|
||||
complete -c watchexec -l project-origin -d 'Set the project origin' -r -f -a "(__fish_complete_directories)"
|
||||
complete -c watchexec -l workdir -d 'Set the working directory' -r -f -a "(__fish_complete_directories)"
|
||||
complete -c watchexec -s e -l exts -d 'Filename extensions to filter to' -r
|
||||
complete -c watchexec -s f -l filter -d 'Filename patterns to filter to' -r
|
||||
complete -c watchexec -l filter-file -d 'Files to load filters from' -r -F
|
||||
complete -c watchexec -s j -l filter-prog -d '[experimental] Filter programs' -r
|
||||
complete -c watchexec -s i -l ignore -d 'Filename patterns to filter out' -r
|
||||
complete -c watchexec -l ignore-file -d 'Files to load ignores from' -r -F
|
||||
complete -c watchexec -l fs-events -d 'Filesystem events to filter to' -r -f -a "{access\t'',create\t'',remove\t'',rename\t'',modify\t'',metadata\t''}"
|
||||
complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a "{bash\t'',elvish\t'',fish\t'',nu\t'',powershell\t'',zsh\t''}"
|
||||
complete -c watchexec -l log-file -d 'Write diagnostic logs to a file' -r -F
|
||||
complete -c watchexec -s r -l restart -d 'Restart the process if it\'s still running'
|
||||
complete -c watchexec -l stdin-quit -d 'Exit when stdin closes'
|
||||
complete -c watchexec -l no-vcs-ignore -d 'Don\'t load gitignores'
|
||||
complete -c watchexec -l no-project-ignore -d 'Don\'t load project-local ignores'
|
||||
complete -c watchexec -l no-global-ignore -d 'Don\'t load global ignores'
|
||||
complete -c watchexec -l no-default-ignore -d 'Don\'t use internal default ignores'
|
||||
complete -c watchexec -l no-discover-ignore -d 'Don\'t discover ignore files at all'
|
||||
complete -c watchexec -l ignore-nothing -d 'Don\'t ignore anything at all'
|
||||
complete -c watchexec -s p -l postpone -d 'Wait until first change before running command'
|
||||
complete -c watchexec -s n -d 'Shorthand for \'--shell=none\''
|
||||
complete -c watchexec -l no-environment -d 'Deprecated shorthand for \'--emit-events=none\''
|
||||
complete -c watchexec -l only-emit-events -d 'Only emit events to stdout, run no commands'
|
||||
complete -c watchexec -l no-process-group -d 'Don\'t use a process group'
|
||||
complete -c watchexec -s 1 -d 'Testing only: exit Watchexec after the first run'
|
||||
complete -c watchexec -s N -l notify -d 'Alert when commands start and end'
|
||||
complete -c watchexec -l timings -d 'Print how long the command took to run'
|
||||
complete -c watchexec -s q -l quiet -d 'Don\'t print starting and stopping messages'
|
||||
complete -c watchexec -l bell -d 'Ring the terminal bell on command completion'
|
||||
complete -c watchexec -l no-meta -d 'Don\'t emit fs events for metadata changes'
|
||||
complete -c watchexec -l print-events -d 'Print events that trigger actions'
|
||||
complete -c watchexec -l manual -d 'Show the manual page'
|
||||
complete -c watchexec -s v -l verbose -d 'Set diagnostic log level'
|
||||
complete -c watchexec -s h -l help -d 'Print help (see more with \'--help\')'
|
||||
complete -c watchexec -s V -l version -d 'Print version'
|
90
completions/nu
Normal file
90
completions/nu
Normal file
|
@ -0,0 +1,90 @@
|
|||
module completions {
|
||||
|
||||
def "nu-complete watchexec screen_clear" [] {
|
||||
[ "clear" "reset" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec on_busy_update" [] {
|
||||
[ "queue" "do-nothing" "restart" "signal" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec emit_events_to" [] {
|
||||
[ "environment" "stdio" "file" "json-stdio" "json-file" "none" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec wrap_process" [] {
|
||||
[ "group" "session" "none" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec color" [] {
|
||||
[ "auto" "always" "never" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec filter_fs_events" [] {
|
||||
[ "access" "create" "remove" "rename" "modify" "metadata" ]
|
||||
}
|
||||
|
||||
def "nu-complete watchexec completions" [] {
|
||||
[ "bash" "elvish" "fish" "nu" "powershell" "zsh" ]
|
||||
}
|
||||
|
||||
# Execute commands when watched files change
|
||||
export extern watchexec [
|
||||
...command: string # Command to run on changes
|
||||
--watch(-w): string # Watch a specific file or directory
|
||||
--watch-non-recursive(-W): string # Watch a specific directory, non-recursively
|
||||
--watch-file(-F): string # Watch files and directories from a file
|
||||
--clear(-c): string@"nu-complete watchexec screen_clear" # Clear screen before running command
|
||||
--on-busy-update(-o): string@"nu-complete watchexec on_busy_update" # What to do when receiving events while the command is running
|
||||
--restart(-r) # Restart the process if it's still running
|
||||
--signal(-s): string # Send a signal to the process when it's still running
|
||||
--stop-signal: string # Signal to send to stop the command
|
||||
--stop-timeout: string # Time to wait for the command to exit gracefully
|
||||
--map-signal: string # Translate signals from the OS to signals to send to the command
|
||||
--debounce(-d): string # Time to wait for new events before taking action
|
||||
--stdin-quit # Exit when stdin closes
|
||||
--no-vcs-ignore # Don't load gitignores
|
||||
--no-project-ignore # Don't load project-local ignores
|
||||
--no-global-ignore # Don't load global ignores
|
||||
--no-default-ignore # Don't use internal default ignores
|
||||
--no-discover-ignore # Don't discover ignore files at all
|
||||
--ignore-nothing # Don't ignore anything at all
|
||||
--postpone(-p) # Wait until first change before running command
|
||||
--delay-run: string # Sleep before running the command
|
||||
--poll: string # Poll for filesystem changes
|
||||
--shell: string # Use a different shell
|
||||
-n # Shorthand for '--shell=none'
|
||||
--no-environment # Deprecated shorthand for '--emit-events=none'
|
||||
--emit-events-to: string@"nu-complete watchexec emit_events_to" # Configure event emission
|
||||
--only-emit-events # Only emit events to stdout, run no commands
|
||||
--env(-E): string # Add env vars to the command
|
||||
--no-process-group # Don't use a process group
|
||||
--wrap-process: string@"nu-complete watchexec wrap_process" # Configure how the process is wrapped
|
||||
-1 # Testing only: exit Watchexec after the first run
|
||||
--notify(-N) # Alert when commands start and end
|
||||
--color: string@"nu-complete watchexec color" # When to use terminal colours
|
||||
--timings # Print how long the command took to run
|
||||
--quiet(-q) # Don't print starting and stopping messages
|
||||
--bell # Ring the terminal bell on command completion
|
||||
--project-origin: string # Set the project origin
|
||||
--workdir: string # Set the working directory
|
||||
--exts(-e): string # Filename extensions to filter to
|
||||
--filter(-f): string # Filename patterns to filter to
|
||||
--filter-file: string # Files to load filters from
|
||||
--filter-prog(-j): string # [experimental] Filter programs
|
||||
--ignore(-i): string # Filename patterns to filter out
|
||||
--ignore-file: string # Files to load ignores from
|
||||
--fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to
|
||||
--no-meta # Don't emit fs events for metadata changes
|
||||
--print-events # Print events that trigger actions
|
||||
--manual # Show the manual page
|
||||
--completions: string@"nu-complete watchexec completions" # Generate a shell completions script
|
||||
--verbose(-v) # Set diagnostic log level
|
||||
--log-file: string # Write diagnostic logs to a file
|
||||
--help(-h) # Print help (see more with '--help')
|
||||
--version(-V) # Print version
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
export use completions *
|
101
completions/powershell
Normal file
101
completions/powershell
Normal file
|
@ -0,0 +1,101 @@
|
|||
|
||||
using namespace System.Management.Automation
|
||||
using namespace System.Management.Automation.Language
|
||||
|
||||
Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
|
||||
param($wordToComplete, $commandAst, $cursorPosition)
|
||||
|
||||
$commandElements = $commandAst.CommandElements
|
||||
$command = @(
|
||||
'watchexec'
|
||||
for ($i = 1; $i -lt $commandElements.Count; $i++) {
|
||||
$element = $commandElements[$i]
|
||||
if ($element -isnot [StringConstantExpressionAst] -or
|
||||
$element.StringConstantType -ne [StringConstantType]::BareWord -or
|
||||
$element.Value.StartsWith('-') -or
|
||||
$element.Value -eq $wordToComplete) {
|
||||
break
|
||||
}
|
||||
$element.Value
|
||||
}) -join ';'
|
||||
|
||||
$completions = @(switch ($command) {
|
||||
'watchexec' {
|
||||
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
|
||||
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
|
||||
[CompletionResult]::new('-W', '-W ', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
|
||||
[CompletionResult]::new('--watch-non-recursive', '--watch-non-recursive', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
|
||||
[CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
|
||||
[CompletionResult]::new('--watch-file', '--watch-file', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
|
||||
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Clear screen before running command')
|
||||
[CompletionResult]::new('--clear', '--clear', [CompletionResultType]::ParameterName, 'Clear screen before running command')
|
||||
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
|
||||
[CompletionResult]::new('--on-busy-update', '--on-busy-update', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
|
||||
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
|
||||
[CompletionResult]::new('--signal', '--signal', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
|
||||
[CompletionResult]::new('--stop-signal', '--stop-signal', [CompletionResultType]::ParameterName, 'Signal to send to stop the command')
|
||||
[CompletionResult]::new('--stop-timeout', '--stop-timeout', [CompletionResultType]::ParameterName, 'Time to wait for the command to exit gracefully')
|
||||
[CompletionResult]::new('--map-signal', '--map-signal', [CompletionResultType]::ParameterName, 'Translate signals from the OS to signals to send to the command')
|
||||
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
|
||||
[CompletionResult]::new('--debounce', '--debounce', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
|
||||
[CompletionResult]::new('--delay-run', '--delay-run', [CompletionResultType]::ParameterName, 'Sleep before running the command')
|
||||
[CompletionResult]::new('--poll', '--poll', [CompletionResultType]::ParameterName, 'Poll for filesystem changes')
|
||||
[CompletionResult]::new('--shell', '--shell', [CompletionResultType]::ParameterName, 'Use a different shell')
|
||||
[CompletionResult]::new('--emit-events-to', '--emit-events-to', [CompletionResultType]::ParameterName, 'Configure event emission')
|
||||
[CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Add env vars to the command')
|
||||
[CompletionResult]::new('--env', '--env', [CompletionResultType]::ParameterName, 'Add env vars to the command')
|
||||
[CompletionResult]::new('--wrap-process', '--wrap-process', [CompletionResultType]::ParameterName, 'Configure how the process is wrapped')
|
||||
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to use terminal colours')
|
||||
[CompletionResult]::new('--project-origin', '--project-origin', [CompletionResultType]::ParameterName, 'Set the project origin')
|
||||
[CompletionResult]::new('--workdir', '--workdir', [CompletionResultType]::ParameterName, 'Set the working directory')
|
||||
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
|
||||
[CompletionResult]::new('--exts', '--exts', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
|
||||
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
|
||||
[CompletionResult]::new('--filter', '--filter', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
|
||||
[CompletionResult]::new('--filter-file', '--filter-file', [CompletionResultType]::ParameterName, 'Files to load filters from')
|
||||
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, '[experimental] Filter programs')
|
||||
[CompletionResult]::new('--filter-prog', '--filter-prog', [CompletionResultType]::ParameterName, '[experimental] Filter programs')
|
||||
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
|
||||
[CompletionResult]::new('--ignore', '--ignore', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
|
||||
[CompletionResult]::new('--ignore-file', '--ignore-file', [CompletionResultType]::ParameterName, 'Files to load ignores from')
|
||||
[CompletionResult]::new('--fs-events', '--fs-events', [CompletionResultType]::ParameterName, 'Filesystem events to filter to')
|
||||
[CompletionResult]::new('--completions', '--completions', [CompletionResultType]::ParameterName, 'Generate a shell completions script')
|
||||
[CompletionResult]::new('--log-file', '--log-file', [CompletionResultType]::ParameterName, 'Write diagnostic logs to a file')
|
||||
[CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
|
||||
[CompletionResult]::new('--restart', '--restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
|
||||
[CompletionResult]::new('--stdin-quit', '--stdin-quit', [CompletionResultType]::ParameterName, 'Exit when stdin closes')
|
||||
[CompletionResult]::new('--no-vcs-ignore', '--no-vcs-ignore', [CompletionResultType]::ParameterName, 'Don''t load gitignores')
|
||||
[CompletionResult]::new('--no-project-ignore', '--no-project-ignore', [CompletionResultType]::ParameterName, 'Don''t load project-local ignores')
|
||||
[CompletionResult]::new('--no-global-ignore', '--no-global-ignore', [CompletionResultType]::ParameterName, 'Don''t load global ignores')
|
||||
[CompletionResult]::new('--no-default-ignore', '--no-default-ignore', [CompletionResultType]::ParameterName, 'Don''t use internal default ignores')
|
||||
[CompletionResult]::new('--no-discover-ignore', '--no-discover-ignore', [CompletionResultType]::ParameterName, 'Don''t discover ignore files at all')
|
||||
[CompletionResult]::new('--ignore-nothing', '--ignore-nothing', [CompletionResultType]::ParameterName, 'Don''t ignore anything at all')
|
||||
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
|
||||
[CompletionResult]::new('--postpone', '--postpone', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
|
||||
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Shorthand for ''--shell=none''')
|
||||
[CompletionResult]::new('--no-environment', '--no-environment', [CompletionResultType]::ParameterName, 'Deprecated shorthand for ''--emit-events=none''')
|
||||
[CompletionResult]::new('--only-emit-events', '--only-emit-events', [CompletionResultType]::ParameterName, 'Only emit events to stdout, run no commands')
|
||||
[CompletionResult]::new('--no-process-group', '--no-process-group', [CompletionResultType]::ParameterName, 'Don''t use a process group')
|
||||
[CompletionResult]::new('-1', '-1', [CompletionResultType]::ParameterName, 'Testing only: exit Watchexec after the first run')
|
||||
[CompletionResult]::new('-N', '-N ', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
|
||||
[CompletionResult]::new('--notify', '--notify', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
|
||||
[CompletionResult]::new('--timings', '--timings', [CompletionResultType]::ParameterName, 'Print how long the command took to run')
|
||||
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
|
||||
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
|
||||
[CompletionResult]::new('--bell', '--bell', [CompletionResultType]::ParameterName, 'Ring the terminal bell on command completion')
|
||||
[CompletionResult]::new('--no-meta', '--no-meta', [CompletionResultType]::ParameterName, 'Don''t emit fs events for metadata changes')
|
||||
[CompletionResult]::new('--print-events', '--print-events', [CompletionResultType]::ParameterName, 'Print events that trigger actions')
|
||||
[CompletionResult]::new('--manual', '--manual', [CompletionResultType]::ParameterName, 'Show the manual page')
|
||||
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
|
||||
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
|
||||
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
|
||||
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
|
||||
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
|
||||
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
||||
Sort-Object -Property ListItemText
|
||||
}
|
127
completions/zsh
127
completions/zsh
|
@ -1,36 +1,103 @@
|
|||
#compdef watchexec
|
||||
|
||||
setopt localoptions extended_glob
|
||||
autoload -U is-at-least
|
||||
|
||||
local cmd
|
||||
local -a args
|
||||
local -a _comp_priv_prefix
|
||||
_watchexec() {
|
||||
typeset -A opt_args
|
||||
typeset -a _arguments_options
|
||||
local ret=1
|
||||
|
||||
cmd="$words[1]"
|
||||
if is-at-least 5.2; then
|
||||
_arguments_options=(-s -S -C)
|
||||
else
|
||||
_arguments_options=(-s -C)
|
||||
fi
|
||||
|
||||
args=(
|
||||
'(-c --clear)'{-c,--clear}'[Clear screen before executing command]'
|
||||
'(-h --help)'{-h,--help}'[Prints help information]'
|
||||
'(-k --kill)'{-k,--kill}'[Send SIGKILL to child processes (deprecated, use -s SIGKILL instead)]'
|
||||
'(-n --no-shell)'{-n,--no-shell}'[Do not wrap command in ''sh -c'' resp. ''cmd.exe /C'']'
|
||||
'--no-environment[Do not set WATCHEXEC_*_PATH environment variables for child process]'
|
||||
'--no-meta[Ignore metadata changes]'
|
||||
'(-p --postpone)'{-p,--postpone}'[Wait until first change to execute command]'
|
||||
'(-r --restart)'{-r,--restart}'[Restart the process if it''s still running]'
|
||||
'(-V --version)'{-V,--version}'[Prints version information]'
|
||||
'(-v --verbose)'{-v,--verbose}'[Print debugging messages to stderr]'
|
||||
'(-d --debounce)'{-d+,--debounce=}'[Set the timeout between detected change and command execution, defaults to 500ms]:milliseconds'
|
||||
'(-e --exts)'{-e+,--exts=}'[Comma-separated list of file extensions to watch (js,css,html)]:extensions'
|
||||
'(-f --filter)'{-f+,--filter=}'[Ignore all modifications except those matching the pattern]:pattern'
|
||||
'(-i --ignore)'{-i+,--ignore=}'[Ignore modifications to paths matching the pattern]:pattern'
|
||||
'(-w --watch)'{-w+,--watch=}'[Watch a specific directory]:path:_path_files -/'
|
||||
'(-s --signal)'{-s+,--signal=}'[Send signal to process upon changes, e.g. SIGHUP]:signal'
|
||||
'--force-poll=[Forces polling mode]:interval'
|
||||
'--no-ignore[Skip auto-loading of ignore files (.gitignore, .ignore, etc.) for filtering]'
|
||||
'--no-default-ignore[Skip auto-ignoring of commonly ignored globs]'
|
||||
'--no-vcs-ignore[Skip auto-loading of .gitignore files for filtering]'
|
||||
'(-)1:command: _command_names -e'
|
||||
'*::arguments:{ _comp_priv_prefix=( $cmd -n ${(kv)opt_args[-u]} ) ; _normal }'
|
||||
)
|
||||
local context curcontext="$curcontext" state line
|
||||
_arguments "${_arguments_options[@]}" : \
|
||||
'*-w+[Watch a specific file or directory]:PATH:_files' \
|
||||
'*--watch=[Watch a specific file or directory]:PATH:_files' \
|
||||
'*-W+[Watch a specific directory, non-recursively]:PATH:_files' \
|
||||
'*--watch-non-recursive=[Watch a specific directory, non-recursively]:PATH:_files' \
|
||||
'-F+[Watch files and directories from a file]:PATH:_files' \
|
||||
'--watch-file=[Watch files and directories from a file]:PATH:_files' \
|
||||
'-c+[Clear screen before running command]' \
|
||||
'--clear=[Clear screen before running command]' \
|
||||
'-o+[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
|
||||
'--on-busy-update=[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
|
||||
'(-r --restart)-s+[Send a signal to the process when it'\''s still running]:SIGNAL: ' \
|
||||
'(-r --restart)--signal=[Send a signal to the process when it'\''s still running]:SIGNAL: ' \
|
||||
'--stop-signal=[Signal to send to stop the command]:SIGNAL: ' \
|
||||
'--stop-timeout=[Time to wait for the command to exit gracefully]:TIMEOUT: ' \
|
||||
'*--map-signal=[Translate signals from the OS to signals to send to the command]:SIGNAL:SIGNAL: ' \
|
||||
'-d+[Time to wait for new events before taking action]:TIMEOUT: ' \
|
||||
'--debounce=[Time to wait for new events before taking action]:TIMEOUT: ' \
|
||||
'--delay-run=[Sleep before running the command]:DURATION: ' \
|
||||
'--poll=[Poll for filesystem changes]' \
|
||||
'--shell=[Use a different shell]:SHELL: ' \
|
||||
'--emit-events-to=[Configure event emission]:MODE:(environment stdio file json-stdio json-file none)' \
|
||||
'*-E+[Add env vars to the command]:KEY=VALUE: ' \
|
||||
'*--env=[Add env vars to the command]:KEY=VALUE: ' \
|
||||
'--wrap-process=[Configure how the process is wrapped]:MODE:(group session none)' \
|
||||
'--color=[When to use terminal colours]:MODE:(auto always never)' \
|
||||
'--project-origin=[Set the project origin]:DIRECTORY:_files -/' \
|
||||
'--workdir=[Set the working directory]:DIRECTORY:_files -/' \
|
||||
'*-e+[Filename extensions to filter to]:EXTENSIONS: ' \
|
||||
'*--exts=[Filename extensions to filter to]:EXTENSIONS: ' \
|
||||
'*-f+[Filename patterns to filter to]:PATTERN: ' \
|
||||
'*--filter=[Filename patterns to filter to]:PATTERN: ' \
|
||||
'*--filter-file=[Files to load filters from]:PATH:_files' \
|
||||
'*-j+[\[experimental\] Filter programs]:EXPRESSION: ' \
|
||||
'*--filter-prog=[\[experimental\] Filter programs]:EXPRESSION: ' \
|
||||
'*-i+[Filename patterns to filter out]:PATTERN: ' \
|
||||
'*--ignore=[Filename patterns to filter out]:PATTERN: ' \
|
||||
'*--ignore-file=[Files to load ignores from]:PATH:_files' \
|
||||
'*--fs-events=[Filesystem events to filter to]:EVENTS:(access create remove rename modify metadata)' \
|
||||
'(--manual)--completions=[Generate a shell completions script]:COMPLETIONS:(bash elvish fish nu powershell zsh)' \
|
||||
'--log-file=[Write diagnostic logs to a file]' \
|
||||
'(-o --on-busy-update)-r[Restart the process if it'\''s still running]' \
|
||||
'(-o --on-busy-update)--restart[Restart the process if it'\''s still running]' \
|
||||
'--stdin-quit[Exit when stdin closes]' \
|
||||
'--no-vcs-ignore[Don'\''t load gitignores]' \
|
||||
'--no-project-ignore[Don'\''t load project-local ignores]' \
|
||||
'--no-global-ignore[Don'\''t load global ignores]' \
|
||||
'--no-default-ignore[Don'\''t use internal default ignores]' \
|
||||
'--no-discover-ignore[Don'\''t discover ignore files at all]' \
|
||||
'--ignore-nothing[Don'\''t ignore anything at all]' \
|
||||
'-p[Wait until first change before running command]' \
|
||||
'--postpone[Wait until first change before running command]' \
|
||||
'-n[Shorthand for '\''--shell=none'\'']' \
|
||||
'--no-environment[Deprecated shorthand for '\''--emit-events=none'\'']' \
|
||||
'(--completions --manual)--only-emit-events[Only emit events to stdout, run no commands]' \
|
||||
'--no-process-group[Don'\''t use a process group]' \
|
||||
'-1[Testing only\: exit Watchexec after the first run]' \
|
||||
'-N[Alert when commands start and end]' \
|
||||
'--notify[Alert when commands start and end]' \
|
||||
'--timings[Print how long the command took to run]' \
|
||||
'-q[Don'\''t print starting and stopping messages]' \
|
||||
'--quiet[Don'\''t print starting and stopping messages]' \
|
||||
'--bell[Ring the terminal bell on command completion]' \
|
||||
'(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \
|
||||
'--print-events[Print events that trigger actions]' \
|
||||
'(--completions)--manual[Show the manual page]' \
|
||||
'*-v[Set diagnostic log level]' \
|
||||
'*--verbose[Set diagnostic log level]' \
|
||||
'-h[Print help (see more with '\''--help'\'')]' \
|
||||
'--help[Print help (see more with '\''--help'\'')]' \
|
||||
'-V[Print version]' \
|
||||
'--version[Print version]' \
|
||||
'*::command -- Command to run on changes:_cmdstring' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
_arguments -s -S $args
|
||||
(( $+functions[_watchexec_commands] )) ||
|
||||
_watchexec_commands() {
|
||||
local commands; commands=()
|
||||
_describe -t commands 'watchexec commands' commands "$@"
|
||||
}
|
||||
|
||||
if [ "$funcstack[1]" = "_watchexec" ]; then
|
||||
_watchexec "$@"
|
||||
else
|
||||
compdef _watchexec watchexec
|
||||
fi
|
||||
|
|
27
crates/bosion/CHANGELOG.md
Normal file
27
crates/bosion/CHANGELOG.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Changelog
|
||||
|
||||
## Next (YYYY-MM-DD)
|
||||
|
||||
## v1.1.1 (2024-10-14)
|
||||
|
||||
- Deps: gix 0.66
|
||||
|
||||
## v1.1.0 (2024-05-16)
|
||||
|
||||
- Add `git-describe` support (#832, by @lu-zero)
|
||||
|
||||
## v1.0.3 (2024-04-20)
|
||||
|
||||
- Deps: gix 0.62
|
||||
|
||||
## v1.0.2 (2023-11-26)
|
||||
|
||||
- Deps: upgrade to gix 0.55
|
||||
|
||||
## v1.0.1 (2023-07-02)
|
||||
|
||||
- Deps: upgrade to gix 0.44
|
||||
|
||||
## v1.0.0 (2023-03-05)
|
||||
|
||||
- Initial release.
|
40
crates/bosion/Cargo.toml
Normal file
40
crates/bosion/Cargo.toml
Normal file
|
@ -0,0 +1,40 @@
|
|||
[package]
|
||||
name = "bosion"
|
||||
version = "1.1.1"
|
||||
|
||||
authors = ["Félix Saparelli <felix@passcod.name>"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
description = "Gather build information for verbose versions flags"
|
||||
keywords = ["version", "git", "verbose", "long"]
|
||||
|
||||
documentation = "https://docs.rs/bosion"
|
||||
repository = "https://github.com/watchexec/watchexec"
|
||||
readme = "README.md"
|
||||
|
||||
rust-version = "1.64.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies.time]
|
||||
version = "0.3.30"
|
||||
features = ["macros", "formatting"]
|
||||
|
||||
[dependencies.gix]
|
||||
version = "0.66.0"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["revision"]
|
||||
|
||||
[features]
|
||||
default = ["git", "reproducible", "std"]
|
||||
|
||||
### Read from git repo, provide GIT_* vars
|
||||
git = ["dep:gix"]
|
||||
|
||||
### Read from SOURCE_DATE_EPOCH when available
|
||||
reproducible = []
|
||||
|
||||
### Provide a long_version_with() function to add extra info
|
||||
###
|
||||
### Specifically this is std support for the _using_ crate, not for the bosion crate itself. It's
|
||||
### assumed that the bosion crate is always std, as it runs in build.rs.
|
||||
std = []
|
147
crates/bosion/README.md
Normal file
147
crates/bosion/README.md
Normal file
|
@ -0,0 +1,147 @@
|
|||
# Bosion
|
||||
|
||||
_Gather build information for verbose versions flags._
|
||||
|
||||
- **[API documentation][docs]**.
|
||||
- Licensed under [Apache 2.0][license] or [MIT](https://passcod.mit-license.org).
|
||||
- Status: maintained.
|
||||
|
||||
[docs]: https://docs.rs/bosion
|
||||
[license]: ../../LICENSE
|
||||
|
||||
## Quick start
|
||||
|
||||
In your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[build-dependencies]
|
||||
bosion = "1.1.1"
|
||||
```
|
||||
|
||||
In your `build.rs`:
|
||||
|
||||
```rust ,no_run
|
||||
fn main() {
|
||||
bosion::gather();
|
||||
}
|
||||
```
|
||||
|
||||
In your `src/main.rs`:
|
||||
|
||||
```rust ,ignore
|
||||
include!(env!("BOSION_PATH"));
|
||||
|
||||
fn main() {
|
||||
// default output, like rustc -Vv
|
||||
println!("{}", Bosion::LONG_VERSION);
|
||||
|
||||
// with additional fields
|
||||
println!("{}", Bosion::long_version_with(&[
|
||||
("custom data", "value"),
|
||||
("LLVM version", "15.0.6"),
|
||||
]));
|
||||
|
||||
// enabled features like +feature +an-other
|
||||
println!("{}", Bosion::CRATE_FEATURE_STRING);
|
||||
|
||||
// the raw data
|
||||
println!("{}", Bosion::GIT_COMMIT_HASH);
|
||||
println!("{}", Bosion::GIT_COMMIT_SHORTHASH);
|
||||
println!("{}", Bosion::GIT_COMMIT_DATE);
|
||||
println!("{}", Bosion::GIT_COMMIT_DATETIME);
|
||||
println!("{}", Bosion::CRATE_VERSION);
|
||||
println!("{:?}", Bosion::CRATE_FEATURES);
|
||||
println!("{}", Bosion::BUILD_DATE);
|
||||
println!("{}", Bosion::BUILD_DATETIME);
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced usage
|
||||
|
||||
Generating a struct with public visibility:
|
||||
|
||||
```rust ,no_run
|
||||
// build.rs
|
||||
bosion::gather_pub();
|
||||
```
|
||||
|
||||
Customising the output file and struct names:
|
||||
|
||||
```rust ,no_run
|
||||
// build.rs
|
||||
bosion::gather_to("buildinfo.rs", "Build", /* public? */ false);
|
||||
```
|
||||
|
||||
Outputting build-time environment variables instead of source:
|
||||
|
||||
```rust ,ignore
|
||||
// build.rs
|
||||
bosion::gather_to_env();
|
||||
|
||||
// src/main.rs
|
||||
fn main() {
|
||||
println!("{}", env!("BOSION_GIT_COMMIT_HASH"));
|
||||
println!("{}", env!("BOSION_GIT_COMMIT_SHORTHASH"));
|
||||
println!("{}", env!("BOSION_GIT_COMMIT_DATE"));
|
||||
println!("{}", env!("BOSION_GIT_COMMIT_DATETIME"));
|
||||
println!("{}", env!("BOSION_BUILD_DATE"));
|
||||
println!("{}", env!("BOSION_BUILD_DATETIME"));
|
||||
println!("{}", env!("BOSION_CRATE_VERSION"));
|
||||
println!("{}", env!("BOSION_CRATE_FEATURES")); // comma-separated
|
||||
}
|
||||
```
|
||||
|
||||
Custom env prefix:
|
||||
|
||||
```rust ,no_run
|
||||
// build.rs
|
||||
bosion::gather_to_env_with_prefix("MYAPP_");
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- `reproducible`: reads [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) (default).
|
||||
- `git`: enables gathering git information (default).
|
||||
- `std`: enables the `long_version_with` method (default).
|
||||
Specifically, this is about the downstream crate's std support, not Bosion's, which always requires std.
|
||||
|
||||
## Why not...?
|
||||
|
||||
- [bugreport](https://github.com/sharkdp/bugreport): runtime library, for bug information.
|
||||
- [git-testament](https://github.com/kinnison/git-testament): uses the `git` CLI instead of gitoxide.
|
||||
- [human-panic](https://github.com/rust-cli/human-panic): runtime library, for panics.
|
||||
- [shadow-rs](https://github.com/baoyachi/shadow-rs): uses libgit2 instead of gitoxide, doesn't rebuild on git changes.
|
||||
- [vergen](https://github.com/rustyhorde/vergen): uses the `git` CLI instead of gitoxide.
|
||||
|
||||
Bosion also requires no dependencies outside of build.rs, and was specifically made for crates
|
||||
installed in a variety of ways, like with `cargo install`, from pre-built binary, from source with
|
||||
git, or from source without git (like a tarball), on a variety of platforms. Its default output with
|
||||
[clap](https://clap.rs) is almost exactly like `rustc -Vv`.
|
||||
|
||||
## Examples
|
||||
|
||||
The [examples](./examples) directory contains a practical and runnable [clap-based example](./examples/clap/), as well
|
||||
as several other crates which are actually used for integration testing.
|
||||
|
||||
Here is the output for the Watchexec CLI:
|
||||
|
||||
```plain
|
||||
watchexec 1.21.1 (5026793 2023-03-05)
|
||||
commit-hash: 5026793a12ff895edf2dafb92111e7bd1767650e
|
||||
commit-date: 2023-03-05
|
||||
build-date: 2023-03-05
|
||||
release: 1.21.1
|
||||
features:
|
||||
```
|
||||
|
||||
For comparison, here's `rustc -Vv`:
|
||||
|
||||
```plain
|
||||
rustc 1.67.1 (d5a82bbd2 2023-02-07)
|
||||
binary: rustc
|
||||
commit-hash: d5a82bbd26e1ad8b7401f6a718a9c57c96905483
|
||||
commit-date: 2023-02-07
|
||||
host: x86_64-unknown-linux-gnu
|
||||
release: 1.67.1
|
||||
LLVM version: 15.0.6
|
||||
```
|
1303
crates/bosion/examples/clap/Cargo.lock
generated
Normal file
1303
crates/bosion/examples/clap/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
crates/bosion/examples/clap/Cargo.toml
Normal file
19
crates/bosion/examples/clap/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "bosion-example-clap"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[features]
|
||||
default = ["foo"]
|
||||
foo = []
|
||||
|
||||
[build-dependencies.bosion]
|
||||
version = "*"
|
||||
path = "../.."
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.1.8"
|
||||
features = ["cargo", "derive"]
|
3
crates/bosion/examples/clap/build.rs
Normal file
3
crates/bosion/examples/clap/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
bosion::gather();
|
||||
}
|
41
crates/bosion/examples/clap/src/main.rs
Normal file
41
crates/bosion/examples/clap/src/main.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use clap::Parser;
|
||||
|
||||
include!(env!("BOSION_PATH"));
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(version, long_version = Bosion::LONG_VERSION)]
|
||||
struct Args {
|
||||
#[clap(long)]
|
||||
extras: bool,
|
||||
|
||||
#[clap(long)]
|
||||
features: bool,
|
||||
|
||||
#[clap(long)]
|
||||
dates: bool,
|
||||
|
||||
#[clap(long)]
|
||||
describe: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.extras {
|
||||
println!(
|
||||
"{}",
|
||||
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3"),])
|
||||
);
|
||||
} else if args.features {
|
||||
println!("Features: {}", Bosion::CRATE_FEATURE_STRING);
|
||||
} else if args.dates {
|
||||
println!("commit date: {}", Bosion::GIT_COMMIT_DATE);
|
||||
println!("commit datetime: {}", Bosion::GIT_COMMIT_DATETIME);
|
||||
println!("build date: {}", Bosion::BUILD_DATE);
|
||||
println!("build datetime: {}", Bosion::BUILD_DATETIME);
|
||||
} else if args.describe {
|
||||
println!("commit description: {}", Bosion::GIT_COMMIT_DESCRIPTION);
|
||||
} else {
|
||||
println!("{}", Bosion::LONG_VERSION);
|
||||
}
|
||||
}
|
1296
crates/bosion/examples/default/Cargo.lock
generated
Normal file
1296
crates/bosion/examples/default/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
20
crates/bosion/examples/default/Cargo.toml
Normal file
20
crates/bosion/examples/default/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
name = "bosion-test-default"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[features]
|
||||
default = ["foo"]
|
||||
foo = []
|
||||
|
||||
[build-dependencies.bosion]
|
||||
version = "*"
|
||||
path = "../.."
|
||||
|
||||
[dependencies]
|
||||
leon = { version = "2.0.1", default-features = false }
|
||||
snapbox = "0.5.9"
|
||||
time = { version = "0.3.30", features = ["formatting", "macros"] }
|
3
crates/bosion/examples/default/build.rs
Normal file
3
crates/bosion/examples/default/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
bosion::gather();
|
||||
}
|
68
crates/bosion/examples/default/src/common.rs
Normal file
68
crates/bosion/examples/default/src/common.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
#[cfg(test)]
|
||||
pub(crate) fn git_commit_info(format: &str) -> String {
|
||||
let output = std::process::Command::new("git")
|
||||
.arg("show")
|
||||
.arg("--no-notes")
|
||||
.arg("--no-patch")
|
||||
.arg(format!("--pretty=format:{format}"))
|
||||
.output()
|
||||
.expect("git");
|
||||
|
||||
String::from_utf8(output.stdout)
|
||||
.expect("git")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_snapshot {
|
||||
($name:ident, $actual:expr) => {
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn $name() {
|
||||
use std::str::FromStr;
|
||||
let gittime = ::time::OffsetDateTime::from_unix_timestamp(
|
||||
i64::from_str(&crate::common::git_commit_info("%ct")).expect("git i64"),
|
||||
)
|
||||
.expect("git time");
|
||||
|
||||
::snapbox::Assert::new().matches(
|
||||
::leon::Template::parse(
|
||||
std::fs::read_to_string(format!("../snapshots/{}.txt", stringify!($name)))
|
||||
.expect("read file")
|
||||
.trim(),
|
||||
)
|
||||
.expect("leon parse")
|
||||
.render(&[
|
||||
(
|
||||
"today date".to_string(),
|
||||
::time::OffsetDateTime::now_utc()
|
||||
.format(::time::macros::format_description!("[year]-[month]-[day]"))
|
||||
.unwrap(),
|
||||
),
|
||||
("git hash".to_string(), crate::common::git_commit_info("%H")),
|
||||
(
|
||||
"git shorthash".to_string(),
|
||||
crate::common::git_commit_info("%h"),
|
||||
),
|
||||
(
|
||||
"git date".to_string(),
|
||||
gittime
|
||||
.format(::time::macros::format_description!("[year]-[month]-[day]"))
|
||||
.expect("git date format"),
|
||||
),
|
||||
(
|
||||
"git datetime".to_string(),
|
||||
gittime
|
||||
.format(::time::macros::format_description!(
|
||||
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
||||
))
|
||||
.expect("git time format"),
|
||||
),
|
||||
])
|
||||
.expect("leon render"),
|
||||
$actual,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
27
crates/bosion/examples/default/src/main.rs
Normal file
27
crates/bosion/examples/default/src/main.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
include!(env!("BOSION_PATH"));
|
||||
|
||||
mod common;
|
||||
fn main() {}
|
||||
|
||||
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
|
||||
|
||||
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
|
||||
|
||||
test_snapshot!(build_date, Bosion::BUILD_DATE);
|
||||
|
||||
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
|
||||
|
||||
test_snapshot!(git_commit_hash, Bosion::GIT_COMMIT_HASH);
|
||||
|
||||
test_snapshot!(git_commit_shorthash, Bosion::GIT_COMMIT_SHORTHASH);
|
||||
|
||||
test_snapshot!(git_commit_date, Bosion::GIT_COMMIT_DATE);
|
||||
|
||||
test_snapshot!(git_commit_datetime, Bosion::GIT_COMMIT_DATETIME);
|
||||
|
||||
test_snapshot!(default_long_version, Bosion::LONG_VERSION);
|
||||
|
||||
test_snapshot!(
|
||||
default_long_version_with,
|
||||
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
|
||||
);
|
336
crates/bosion/examples/no-git/Cargo.lock
generated
Normal file
336
crates/bosion/examples/no-git/Cargo.lock
generated
Normal file
|
@ -0,0 +1,336 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bosion"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bosion-test-no-git"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bosion",
|
||||
"leon",
|
||||
"snapbox",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "leon"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52df920dfe9751d43501ff2ee12dd81c457d9e810d3f64b5200ee461fe73800b"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
|
||||
|
||||
[[package]]
|
||||
name = "snapbox"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37d101fcafc8e73748fd8a1b7048f5979f93d372fd17027d7724c1643bc379b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"normalize-line-endings",
|
||||
"similar",
|
||||
"snapbox-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snapbox-macros"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
22
crates/bosion/examples/no-git/Cargo.toml
Normal file
22
crates/bosion/examples/no-git/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "bosion-test-no-git"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[features]
|
||||
default = ["foo"]
|
||||
foo = []
|
||||
|
||||
[build-dependencies.bosion]
|
||||
version = "*"
|
||||
path = "../.."
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
|
||||
[dependencies]
|
||||
leon = { version = "2.0.1", default-features = false }
|
||||
snapbox = "0.5.9"
|
||||
time = { version = "0.3.30", features = ["formatting", "macros"] }
|
3
crates/bosion/examples/no-git/build.rs
Normal file
3
crates/bosion/examples/no-git/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
bosion::gather();
|
||||
}
|
20
crates/bosion/examples/no-git/src/main.rs
Normal file
20
crates/bosion/examples/no-git/src/main.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
include!(env!("BOSION_PATH"));
|
||||
|
||||
#[path = "../../default/src/common.rs"]
|
||||
mod common;
|
||||
fn main() {}
|
||||
|
||||
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
|
||||
|
||||
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
|
||||
|
||||
test_snapshot!(build_date, Bosion::BUILD_DATE);
|
||||
|
||||
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
|
||||
|
||||
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
|
||||
|
||||
test_snapshot!(
|
||||
no_git_long_version_with,
|
||||
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
|
||||
);
|
336
crates/bosion/examples/no-std/Cargo.lock
generated
Normal file
336
crates/bosion/examples/no-std/Cargo.lock
generated
Normal file
|
@ -0,0 +1,336 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bosion"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bosion-test-no-std"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bosion",
|
||||
"leon",
|
||||
"snapbox",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "leon"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52df920dfe9751d43501ff2ee12dd81c457d9e810d3f64b5200ee461fe73800b"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
|
||||
|
||||
[[package]]
|
||||
name = "snapbox"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37d101fcafc8e73748fd8a1b7048f5979f93d372fd17027d7724c1643bc379b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"normalize-line-endings",
|
||||
"similar",
|
||||
"snapbox-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snapbox-macros"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
27
crates/bosion/examples/no-std/Cargo.toml
Normal file
27
crates/bosion/examples/no-std/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "bosion-test-no-std"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
[workspace]
|
||||
|
||||
[features]
|
||||
default = ["foo"]
|
||||
foo = []
|
||||
|
||||
[build-dependencies.bosion]
|
||||
version = "*"
|
||||
path = "../.."
|
||||
default-features = false
|
||||
|
||||
[dependencies]
|
||||
leon = { version = "2.0.1", default-features = false }
|
||||
snapbox = "0.5.9"
|
||||
time = { version = "0.3.30", features = ["formatting", "macros"] }
|
3
crates/bosion/examples/no-std/build.rs
Normal file
3
crates/bosion/examples/no-std/build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
bosion::gather();
|
||||
}
|
32
crates/bosion/examples/no-std/src/main.rs
Normal file
32
crates/bosion/examples/no-std/src/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
#![cfg_attr(not(test), no_main)]
|
||||
#![cfg_attr(not(test), no_std)]
|
||||
|
||||
#[cfg(not(test))]
|
||||
use core::panic::PanicInfo;
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(_panic: &PanicInfo<'_>) -> ! {
|
||||
loop {}
|
||||
}
|
||||
|
||||
include!(env!("BOSION_PATH"));
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../default/src/common.rs"]
|
||||
mod common;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
|
||||
|
||||
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
|
||||
|
||||
test_snapshot!(build_date, Bosion::BUILD_DATE);
|
||||
|
||||
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
|
||||
|
||||
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
|
||||
}
|
1
crates/bosion/examples/snapshots/build_date.txt
Normal file
1
crates/bosion/examples/snapshots/build_date.txt
Normal file
|
@ -0,0 +1 @@
|
|||
{today date}
|
1
crates/bosion/examples/snapshots/build_datetime.txt
Normal file
1
crates/bosion/examples/snapshots/build_datetime.txt
Normal file
|
@ -0,0 +1 @@
|
|||
{today date} [..]
|
4
crates/bosion/examples/snapshots/crate_features.txt
Normal file
4
crates/bosion/examples/snapshots/crate_features.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
"default",
|
||||
"foo",
|
||||
]
|
1
crates/bosion/examples/snapshots/crate_version.txt
Normal file
1
crates/bosion/examples/snapshots/crate_version.txt
Normal file
|
@ -0,0 +1 @@
|
|||
0.1.0
|
|
@ -0,0 +1,6 @@
|
|||
0.1.0 ({git shorthash} {git date}) +foo
|
||||
commit-hash: {git hash}
|
||||
commit-date: {git date}
|
||||
build-date: {today date}
|
||||
release: 0.1.0
|
||||
features: default,foo
|
|
@ -0,0 +1,8 @@
|
|||
0.1.0 ({git shorthash} {git date}) +foo
|
||||
commit-hash: {git hash}
|
||||
commit-date: {git date}
|
||||
build-date: {today date}
|
||||
release: 0.1.0
|
||||
features: default,foo
|
||||
extra: field
|
||||
custom: 1.2.3
|
1
crates/bosion/examples/snapshots/git_commit_date.txt
Normal file
1
crates/bosion/examples/snapshots/git_commit_date.txt
Normal file
|
@ -0,0 +1 @@
|
|||
{git date}
|
1
crates/bosion/examples/snapshots/git_commit_datetime.txt
Normal file
1
crates/bosion/examples/snapshots/git_commit_datetime.txt
Normal file
|
@ -0,0 +1 @@
|
|||
{git datetime}
|
1
crates/bosion/examples/snapshots/git_commit_hash.txt
Normal file
1
crates/bosion/examples/snapshots/git_commit_hash.txt
Normal file
|
@ -0,0 +1 @@
|
|||
{git hash}
|
|
@ -0,0 +1 @@
|
|||
{git shorthash}
|
4
crates/bosion/examples/snapshots/no_git_long_version.txt
Normal file
4
crates/bosion/examples/snapshots/no_git_long_version.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
0.1.0 ({today date}) +foo
|
||||
build-date: {today date}
|
||||
release: 0.1.0
|
||||
features: default,foo
|
|
@ -0,0 +1,6 @@
|
|||
0.1.0 ({today date}) +foo
|
||||
build-date: {today date}
|
||||
release: 0.1.0
|
||||
features: default,foo
|
||||
extra: field
|
||||
custom: 1.2.3
|
17
crates/bosion/release.toml
Normal file
17
crates/bosion/release.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
pre-release-commit-message = "release: bosion v{{version}}"
|
||||
tag-prefix = "bosion-"
|
||||
tag-message = "bosion {{version}}"
|
||||
|
||||
[[pre-release-replacements]]
|
||||
file = "CHANGELOG.md"
|
||||
search = "^## Next.*$"
|
||||
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
|
||||
prerelease = true
|
||||
max = 1
|
||||
|
||||
[[pre-release-replacements]]
|
||||
file = "README.md"
|
||||
search = "^bosion = \".*\"$"
|
||||
replace = "bosion = \"{{version}}\""
|
||||
prerelease = true
|
||||
max = 1
|
11
crates/bosion/run-tests.sh
Executable file
11
crates/bosion/run-tests.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
for test in default no-git no-std; do
|
||||
echo "Testing $test"
|
||||
pushd examples/$test
|
||||
cargo check
|
||||
cargo test
|
||||
popd
|
||||
done
|
172
crates/bosion/src/info.rs
Normal file
172
crates/bosion/src/info.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use std::{env::var, path::PathBuf};
|
||||
|
||||
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
|
||||
|
||||
/// Gathered build-time information
|
||||
///
|
||||
/// This struct contains all the information gathered by `bosion`. It is not meant to be used
|
||||
/// directly under normal circumstances, but is public for documentation purposes and if you wish
|
||||
/// to build your own frontend for whatever reason. In that case, note that no effort has been made
|
||||
/// to make this usable outside of the build.rs environment.
|
||||
///
|
||||
/// The `git` field is only available when the `git` feature is enabled, and if there is a git
|
||||
/// repository to read from. The repository is discovered by walking up the directory tree until one
|
||||
/// is found, which means workspaces or more complex monorepos are automatically supported. If there
|
||||
/// are any errors reading the repository, the `git` field will be `None` and a rustc warning will
|
||||
/// be printed.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Info {
|
||||
/// The crate version, as read from the `CARGO_PKG_VERSION` environment variable.
|
||||
pub crate_version: String,
|
||||
|
||||
/// The crate features, as found by the presence of `CARGO_FEATURE_*` environment variables.
|
||||
///
|
||||
/// These are normalised to lowercase and have underscores replaced by hyphens.
|
||||
pub crate_features: Vec<String>,
|
||||
|
||||
/// The build date, in the format `YYYY-MM-DD`, at UTC.
|
||||
///
|
||||
/// This is either current as of build time, or from the timestamp specified by the
|
||||
/// `SOURCE_DATE_EPOCH` environment variable, for
|
||||
/// [reproducible builds](https://reproducible-builds.org/).
|
||||
pub build_date: String,
|
||||
|
||||
/// The build datetime, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
|
||||
///
|
||||
/// This is either current as of build time, or from the timestamp specified by the
|
||||
/// `SOURCE_DATE_EPOCH` environment variable, for
|
||||
/// [reproducible builds](https://reproducible-builds.org/).
|
||||
pub build_datetime: String,
|
||||
|
||||
/// Git repository information, if available.
|
||||
pub git: Option<GitInfo>,
|
||||
}
|
||||
|
||||
trait ErrString<T> {
|
||||
fn err_string(self) -> Result<T, String>;
|
||||
}
|
||||
|
||||
impl<T, E> ErrString<T> for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Display,
|
||||
{
|
||||
fn err_string(self) -> Result<T, String> {
|
||||
self.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[month]-[day]");
|
||||
const DATETIME_FORMAT: &[FormatItem<'static>] =
|
||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
|
||||
impl Info {
|
||||
/// Gathers build-time information
|
||||
///
|
||||
/// This is not meant to be used directly under normal circumstances, but is public if you wish
|
||||
/// to build your own frontend for whatever reason. In that case, note that no effort has been
|
||||
/// made to make this usable outside of the build.rs environment.
|
||||
pub fn gather() -> Result<Self, String> {
|
||||
let build_date = Self::build_date()?;
|
||||
|
||||
Ok(Self {
|
||||
crate_version: var("CARGO_PKG_VERSION").err_string()?,
|
||||
crate_features: Self::features(),
|
||||
build_date: build_date.format(DATE_FORMAT).err_string()?,
|
||||
build_datetime: build_date.format(DATETIME_FORMAT).err_string()?,
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
git: GitInfo::gather()
|
||||
.map_err(|e| {
|
||||
println!("cargo:warning=git info gathering failed: {e}");
|
||||
})
|
||||
.ok(),
|
||||
#[cfg(not(feature = "git"))]
|
||||
git: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_date() -> Result<OffsetDateTime, String> {
|
||||
if cfg!(feature = "reproducible") {
|
||||
if let Ok(date) = var("SOURCE_DATE_EPOCH") {
|
||||
if let Ok(date) = date.parse::<i64>() {
|
||||
return OffsetDateTime::from_unix_timestamp(date).err_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OffsetDateTime::now_utc())
|
||||
}
|
||||
|
||||
fn features() -> Vec<String> {
|
||||
let mut features = Vec::new();
|
||||
|
||||
for (key, _) in std::env::vars() {
|
||||
if let Some(stripped) = key.strip_prefix("CARGO_FEATURE_") {
|
||||
features.push(stripped.replace('_', "-").to_lowercase().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
features
|
||||
}
|
||||
|
||||
pub(crate) fn set_reruns(&self) {
|
||||
if cfg!(feature = "reproducible") {
|
||||
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
|
||||
}
|
||||
|
||||
if let Some(git) = &self.git {
|
||||
let git_head = git.git_root.join("HEAD");
|
||||
println!("cargo:rerun-if-changed={}", git_head.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Git repository information
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GitInfo {
|
||||
/// The absolute path to the git repository's data folder.
|
||||
///
|
||||
/// In a normal repository, this is `.git`, _not_ the index or working directory.
|
||||
pub git_root: PathBuf,
|
||||
|
||||
/// The full hash of the current commit.
|
||||
///
|
||||
/// Note that this makes no effore to handle dirty working directories, so it may not be
|
||||
/// representative of the current state of the code.
|
||||
pub git_hash: String,
|
||||
|
||||
/// The short hash of the current commit.
|
||||
///
|
||||
/// This is read from git and not truncated manually, so it may be longer than 7 characters.
|
||||
pub git_shorthash: String,
|
||||
|
||||
/// The date of the current commit, in the format `YYYY-MM-DD`, at UTC.
|
||||
pub git_date: String,
|
||||
|
||||
/// The datetime of the current commit, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
|
||||
pub git_datetime: String,
|
||||
|
||||
/// The `git describe` equivalent output
|
||||
pub git_description: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "git")]
|
||||
impl GitInfo {
|
||||
fn gather() -> Result<Self, String> {
|
||||
use std::path::Path;
|
||||
let (path, _) = gix::discover::upwards(Path::new(".")).err_string()?;
|
||||
let repo = gix::discover(path).err_string()?;
|
||||
let head = repo.head_commit().err_string()?;
|
||||
let time = head.time().err_string()?;
|
||||
let timestamp = OffsetDateTime::from_unix_timestamp(time.seconds as _).err_string()?;
|
||||
|
||||
Ok(Self {
|
||||
git_root: repo.path().canonicalize().err_string()?,
|
||||
git_hash: head.id().to_string(),
|
||||
git_shorthash: head.short_id().err_string()?.to_string(),
|
||||
git_date: timestamp.format(DATE_FORMAT).err_string()?,
|
||||
git_datetime: timestamp.format(DATETIME_FORMAT).err_string()?,
|
||||
git_description: head.describe().format().err_string()?.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
263
crates/bosion/src/lib.rs
Normal file
263
crates/bosion/src/lib.rs
Normal file
|
@ -0,0 +1,263 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::{env::var, fs::File, io::Write, path::PathBuf};
|
||||
|
||||
pub use info::*;
|
||||
mod info;
|
||||
|
||||
/// Gather build-time information for the current crate
|
||||
///
|
||||
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
|
||||
/// [`gather_to`] with the most common defaults: it writes to `bosion.rs` a pub(crate) struct named
|
||||
/// `Bosion`.
|
||||
pub fn gather() {
|
||||
gather_to("bosion.rs", "Bosion", false);
|
||||
}
|
||||
|
||||
/// Gather build-time information for the current crate (public visibility)
|
||||
///
|
||||
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
|
||||
/// [`gather_to`]: it writes to `bosion.rs` a pub struct named `Bosion`.
|
||||
pub fn gather_pub() {
|
||||
gather_to("bosion.rs", "Bosion", true);
|
||||
}
|
||||
|
||||
/// Gather build-time information for the current crate (custom output)
|
||||
///
|
||||
/// Gathers a limited set of build-time information for the current crate and writes it to a file.
|
||||
/// The file is always written to the `OUT_DIR` directory, as per Cargo conventions. It contains a
|
||||
/// zero-size struct with a bunch of associated constants containing the gathered information, and a
|
||||
/// `long_version_with` function (when the `std` feature is enabled) that takes a slice of extra
|
||||
/// key-value pairs to append in the same format.
|
||||
///
|
||||
/// `public` controls whether the struct is `pub` (true) or `pub(crate)` (false).
|
||||
///
|
||||
/// The generated code is entirely documented, and will appear in your documentation (in docs.rs, it
|
||||
/// only will if visibility is public).
|
||||
///
|
||||
/// See [`Info`] for a list of gathered data.
|
||||
///
|
||||
/// The constants include all the information from [`Info`], as well as the following:
|
||||
///
|
||||
/// - `LONG_VERSION`: A clap-ready long version string, including the crate version, features, build
|
||||
/// date, and git information when available.
|
||||
/// - `CRATE_FEATURE_STRING`: A string containing the crate features, in the format `+feat1 +feat2`.
|
||||
///
|
||||
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
|
||||
pub fn gather_to(filename: &str, structname: &str, public: bool) {
|
||||
let path = PathBuf::from(var("OUT_DIR").expect("bosion")).join(filename);
|
||||
println!("cargo:rustc-env=BOSION_PATH={}", path.display());
|
||||
|
||||
let info = Info::gather().expect("bosion");
|
||||
info.set_reruns();
|
||||
let Info {
|
||||
crate_version,
|
||||
crate_features,
|
||||
build_date,
|
||||
build_datetime,
|
||||
git,
|
||||
} = info;
|
||||
|
||||
let crate_feature_string = crate_features
|
||||
.iter()
|
||||
.filter(|feat| *feat != "default")
|
||||
.map(|feat| format!("+{feat}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let crate_feature_list = crate_features.join(",");
|
||||
|
||||
let viz = if public { "pub" } else { "pub(crate)" };
|
||||
|
||||
let (git_render, long_version) = if let Some(GitInfo {
|
||||
git_hash,
|
||||
git_shorthash,
|
||||
git_date,
|
||||
git_datetime,
|
||||
git_description,
|
||||
..
|
||||
}) = git
|
||||
{
|
||||
(format!(
|
||||
"
|
||||
/// The git commit hash
|
||||
///
|
||||
/// This is the full hash of the commit that was built. Note that if the repository was
|
||||
/// dirty, this will be the hash of the last commit, not including the changes.
|
||||
pub const GIT_COMMIT_HASH: &'static str = {git_hash:?};
|
||||
|
||||
/// The git commit hash, shortened
|
||||
///
|
||||
/// This is the shortened hash of the commit that was built. Same caveats as with
|
||||
/// `GIT_COMMIT_HASH` apply. The length of the hash is as short as possible while still
|
||||
/// being unambiguous, at build time. For large repositories, this may be longer than 7
|
||||
/// characters.
|
||||
pub const GIT_COMMIT_SHORTHASH: &'static str = {git_shorthash:?};
|
||||
|
||||
/// The git commit date
|
||||
///
|
||||
/// This is the date (`YYYY-MM-DD`) of the commit that was built. Same caveats as with
|
||||
/// `GIT_COMMIT_HASH` apply.
|
||||
pub const GIT_COMMIT_DATE: &'static str = {git_date:?};
|
||||
|
||||
/// The git commit date and time
|
||||
///
|
||||
/// This is the date and time (`YYYY-MM-DD HH:MM:SS`) of the commit that was built. Same
|
||||
/// caveats as with `GIT_COMMIT_HASH` apply.
|
||||
pub const GIT_COMMIT_DATETIME: &'static str = {git_datetime:?};
|
||||
|
||||
/// The git description
|
||||
///
|
||||
/// This is the string equivalent to what `git describe` would output
|
||||
pub const GIT_COMMIT_DESCRIPTION: &'static str = {git_description:?};
|
||||
"
|
||||
), format!("{crate_version} ({git_shorthash} {git_date}) {crate_feature_string}\ncommit-hash: {git_hash}\ncommit-date: {git_date}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
|
||||
} else {
|
||||
(String::new(), format!("{crate_version} ({build_date}) {crate_feature_string}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
|
||||
};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
let long_version_with_fn = r#"
|
||||
/// Returns the long version string with extra information tacked on
|
||||
///
|
||||
/// This is the same as `LONG_VERSION` but takes a slice of key-value pairs to append to the
|
||||
/// end in the same format.
|
||||
pub fn long_version_with(extra: &[(&str, &str)]) -> String {
|
||||
let mut output = Self::LONG_VERSION.to_string();
|
||||
|
||||
for (k, v) in extra {
|
||||
output.push_str(&format!("\n{k}: {v}"));
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
"#;
|
||||
#[cfg(not(feature = "std"))]
|
||||
let long_version_with_fn = "";
|
||||
|
||||
let bosion_version = env!("CARGO_PKG_VERSION");
|
||||
let render = format!(
|
||||
r#"
|
||||
/// Build-time information
|
||||
///
|
||||
/// This struct is generated by the [bosion](https://docs.rs/bosion) crate at build time.
|
||||
///
|
||||
/// Bosion version: {bosion_version}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
{viz} struct {structname};
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl {structname} {{
|
||||
/// Clap-compatible long version string
|
||||
///
|
||||
/// At minimum, this will be the crate version and build date.
|
||||
///
|
||||
/// It presents as a first "summary" line like `crate_version (build_date) features`,
|
||||
/// followed by `key: value` pairs. This is the same format used by `rustc -Vv`.
|
||||
///
|
||||
/// If git info is available, it also includes the git hash, short hash and commit date,
|
||||
/// and swaps the build date for the commit date in the summary line.
|
||||
pub const LONG_VERSION: &'static str = {long_version:?};
|
||||
|
||||
/// The crate version, as reported by Cargo
|
||||
///
|
||||
/// You should probably prefer reading the `CARGO_PKG_VERSION` environment variable.
|
||||
pub const CRATE_VERSION: &'static str = {crate_version:?};
|
||||
|
||||
/// The crate features
|
||||
///
|
||||
/// This is a list of the features that were enabled when this crate was built,
|
||||
/// lowercased and with underscores replaced by hyphens.
|
||||
pub const CRATE_FEATURES: &'static [&'static str] = &{crate_features:?};
|
||||
|
||||
/// The crate features, as a string
|
||||
///
|
||||
/// This is in format `+feature +feature2 +feature3`, lowercased with underscores
|
||||
/// replaced by hyphens.
|
||||
pub const CRATE_FEATURE_STRING: &'static str = {crate_feature_string:?};
|
||||
|
||||
/// The build date
|
||||
///
|
||||
/// This is the date that the crate was built, in the format `YYYY-MM-DD`. If the
|
||||
/// environment variable `SOURCE_DATE_EPOCH` was set, it's used instead of the current
|
||||
/// time, for [reproducible builds](https://reproducible-builds.org/).
|
||||
pub const BUILD_DATE: &'static str = {build_date:?};
|
||||
|
||||
/// The build datetime
|
||||
///
|
||||
/// This is the date and time that the crate was built, in the format
|
||||
/// `YYYY-MM-DD HH:MM:SS`. If the environment variable `SOURCE_DATE_EPOCH` was set, it's
|
||||
/// used instead of the current time, for
|
||||
/// [reproducible builds](https://reproducible-builds.org/).
|
||||
pub const BUILD_DATETIME: &'static str = {build_datetime:?};
|
||||
|
||||
{git_render}
|
||||
|
||||
{long_version_with_fn}
|
||||
}}
|
||||
"#
|
||||
);
|
||||
|
||||
let mut file = File::create(path).expect("bosion");
|
||||
file.write_all(render.as_bytes()).expect("bosion");
|
||||
}
|
||||
|
||||
/// Gather build-time information and write it to the environment
|
||||
///
|
||||
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
|
||||
/// [`gather_to_env_with_prefix`] with the most common default prefix of `BOSION_`.
|
||||
pub fn gather_to_env() {
|
||||
gather_to_env_with_prefix("BOSION_");
|
||||
}
|
||||
|
||||
/// Gather build-time information and write it to the environment
|
||||
///
|
||||
/// Gathers a limited set of build-time information for the current crate and makes it available to
|
||||
/// the crate as build environment variables. This is an alternative to [`include!`]ing a file which
|
||||
/// is generated at build time, like for [`gather`] and variants, which doesn't create any new code
|
||||
/// and doesn't include any information in the binary that you do not explicitly use.
|
||||
///
|
||||
/// The environment variables are prefixed with the given string, which should be generally be
|
||||
/// uppercase and end with an underscore.
|
||||
///
|
||||
/// See [`Info`] for a list of gathered data.
|
||||
///
|
||||
/// Unlike [`gather`], there is no Clap-ready `LONG_VERSION` string, but you can of course generate
|
||||
/// one yourself from the environment variables.
|
||||
///
|
||||
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
|
||||
pub fn gather_to_env_with_prefix(prefix: &str) {
|
||||
let info = Info::gather().expect("bosion");
|
||||
info.set_reruns();
|
||||
let Info {
|
||||
crate_version,
|
||||
crate_features,
|
||||
build_date,
|
||||
build_datetime,
|
||||
git,
|
||||
} = info;
|
||||
|
||||
println!("cargo:rustc-env={prefix}CRATE_VERSION={crate_version}");
|
||||
println!(
|
||||
"cargo:rustc-env={prefix}CRATE_FEATURES={}",
|
||||
crate_features.join(",")
|
||||
);
|
||||
println!("cargo:rustc-env={prefix}BUILD_DATE={build_date}");
|
||||
println!("cargo:rustc-env={prefix}BUILD_DATETIME={build_datetime}");
|
||||
|
||||
if let Some(GitInfo {
|
||||
git_hash,
|
||||
git_shorthash,
|
||||
git_date,
|
||||
git_datetime,
|
||||
git_description,
|
||||
..
|
||||
}) = git
|
||||
{
|
||||
println!("cargo:rustc-env={prefix}GIT_COMMIT_HASH={git_hash}");
|
||||
println!("cargo:rustc-env={prefix}GIT_COMMIT_SHORTHASH={git_shorthash}");
|
||||
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATE={git_date}");
|
||||
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATETIME={git_datetime}");
|
||||
println!("cargo:rustc-env={prefix}GIT_COMMIT_DESCRIPTION={git_description}");
|
||||
}
|
||||
}
|
197
crates/cli/Cargo.toml
Normal file
197
crates/cli/Cargo.toml
Normal file
|
@ -0,0 +1,197 @@
|
|||
[package]
|
||||
name = "watchexec-cli"
|
||||
version = "2.2.0"
|
||||
|
||||
authors = ["Félix Saparelli <felix@passcod.name>", "Matt Green <mattgreenrocks@gmail.com>"]
|
||||
license = "Apache-2.0"
|
||||
description = "Executes commands in response to file modifications"
|
||||
keywords = ["watcher", "filesystem", "cli", "watchexec"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
documentation = "https://watchexec.github.io/docs/#watchexec"
|
||||
homepage = "https://watchexec.github.io"
|
||||
repository = "https://github.com/watchexec/watchexec"
|
||||
readme = "README.md"
|
||||
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "watchexec"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ahash = "0.8.6" # needs to be in sync with jaq's
|
||||
argfile = "0.2.0"
|
||||
chrono = "0.4.31"
|
||||
clap_complete = "4.4.4"
|
||||
clap_complete_nushell = "4.4.2"
|
||||
clap_mangen = "0.2.15"
|
||||
clearscreen = "3.0.0"
|
||||
dashmap = "6.1.0"
|
||||
dirs = "5.0.0"
|
||||
dunce = "1.0.4"
|
||||
futures = "0.3.29"
|
||||
humantime = "2.1.0"
|
||||
indexmap = "2.2.6" # needs to be in sync with jaq's
|
||||
is-terminal = "0.4.4"
|
||||
jaq-core = "1.2.1"
|
||||
jaq-interpret = "1.2.1"
|
||||
jaq-parse = "1.0.2"
|
||||
jaq-std = "1.2.1"
|
||||
jaq-syn = "1.1.0"
|
||||
notify-rust = "4.9.0"
|
||||
once_cell = "1.17.1"
|
||||
serde_json = "1.0.107"
|
||||
tempfile = "3.8.1"
|
||||
termcolor = "1.4.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
which = "6.0.1"
|
||||
|
||||
[dependencies.blake3]
|
||||
version = "1.3.3"
|
||||
features = ["rayon"]
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.4.7"
|
||||
features = ["cargo", "derive", "env", "wrap_help"]
|
||||
|
||||
[dependencies.console-subscriber]
|
||||
version = "0.4.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.eyra]
|
||||
version = "0.19.0"
|
||||
features = ["log", "env_logger"]
|
||||
optional = true
|
||||
|
||||
[dependencies.ignore-files]
|
||||
version = "3.0.2"
|
||||
path = "../ignore-files"
|
||||
|
||||
[dependencies.miette]
|
||||
version = "7.2.0"
|
||||
features = ["fancy"]
|
||||
|
||||
[dependencies.pid1]
|
||||
version = "0.1.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.project-origins]
|
||||
version = "1.4.0"
|
||||
path = "../project-origins"
|
||||
|
||||
[dependencies.watchexec]
|
||||
version = "5.0.0"
|
||||
path = "../lib"
|
||||
|
||||
[dependencies.watchexec-events]
|
||||
version = "4.0.0"
|
||||
path = "../events"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.watchexec-signals]
|
||||
version = "4.0.0"
|
||||
path = "../signals"
|
||||
|
||||
[dependencies.watchexec-filterer-globset]
|
||||
version = "6.0.0"
|
||||
path = "../filterer/globset"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.33.0"
|
||||
features = [
|
||||
"fs",
|
||||
"io-std",
|
||||
"process",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"sync",
|
||||
]
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3.6"
|
||||
features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"json",
|
||||
"tracing-log",
|
||||
"ansi",
|
||||
]
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1.39"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.4.0"
|
||||
|
||||
[build-dependencies.bosion]
|
||||
version = "1.1.1"
|
||||
path = "../bosion"
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-test = "0.2.4"
|
||||
uuid = { workspace = true, features = [ "v4", "fast-rng" ] }
|
||||
rand = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["pid1"]
|
||||
|
||||
## Build using Eyra's pure-Rust libc
|
||||
eyra = ["dep:eyra"]
|
||||
|
||||
## Enables PID1 handling.
|
||||
pid1 = ["dep:pid1"]
|
||||
|
||||
## Enables logging for PID1 handling.
|
||||
pid1-withlog = ["pid1"]
|
||||
|
||||
## For debugging only: enables the Tokio Console.
|
||||
dev-console = ["dep:console-subscriber"]
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/watchexec-{ version }-{ target }.{ archive-format }"
|
||||
bin-dir = "watchexec-{ version }-{ target }/{ bin }{ binary-ext }"
|
||||
pkg-fmt = "txz"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[package.metadata.deb]
|
||||
maintainer = "Félix Saparelli <felix@passcod.name>"
|
||||
license-file = ["../../LICENSE", "0"]
|
||||
section = "utility"
|
||||
depends = "libc6, libgcc-s1" # not needed for musl, but see below
|
||||
# conf-files = [] # look me up when config file lands
|
||||
assets = [
|
||||
["../../target/release/watchexec", "usr/bin/watchexec", "755"],
|
||||
["README.md", "usr/share/doc/watchexec/README", "644"],
|
||||
["../../doc/watchexec.1.md", "usr/share/doc/watchexec/watchexec.1.md", "644"],
|
||||
["../../doc/watchexec.1", "usr/share/man/man1/watchexec.1", "644"],
|
||||
["../../completions/bash", "usr/share/bash-completion/completions/watchexec", "644"],
|
||||
["../../completions/fish", "usr/share/fish/vendor_completions.d/watchexec.fish", "644"],
|
||||
["../../completions/zsh", "usr/share/zsh/site-functions/_watchexec", "644"],
|
||||
["../../doc/logo.svg", "usr/share/icons/hicolor/scalable/apps/watchexec.svg", "644"],
|
||||
]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{ source = "../../target/release/watchexec", dest = "/usr/bin/watchexec", mode = "755" },
|
||||
{ source = "README.md", dest = "/usr/share/doc/watchexec/README", mode = "644", doc = true },
|
||||
{ source = "../../doc/watchexec.1.md", dest = "/usr/share/doc/watchexec/watchexec.1.md", mode = "644", doc = true },
|
||||
{ source = "../../doc/watchexec.1", dest = "/usr/share/man/man1/watchexec.1", mode = "644" },
|
||||
{ source = "../../completions/bash", dest = "/usr/share/bash-completion/completions/watchexec", mode = "644" },
|
||||
{ source = "../../completions/fish", dest = "/usr/share/fish/vendor_completions.d/watchexec.fish", mode = "644" },
|
||||
{ source = "../../completions/zsh", dest = "/usr/share/zsh/site-functions/_watchexec", mode = "644" },
|
||||
{ source = "../../doc/logo.svg", dest = "/usr/share/icons/hicolor/scalable/apps/watchexec.svg", mode = "644" },
|
||||
# set conf = true for config file when that lands
|
||||
]
|
||||
|
||||
auto-req = "disabled"
|
||||
# technically incorrect when using musl, but these are probably
|
||||
# present on every rpm-using system, so let's worry about it if
|
||||
# someone asks.
|
||||
[package.metadata.generate-rpm.requires]
|
||||
glibc = "*"
|
||||
libgcc = "*"
|
197
crates/cli/README.md
Normal file
197
crates/cli/README.md
Normal file
|
@ -0,0 +1,197 @@
|
|||
# Watchexec CLI
|
||||
|
||||
A simple standalone tool that watches a path and runs a command whenever it detects modifications.
|
||||
|
||||
Example use cases:
|
||||
|
||||
* Automatically run unit tests
|
||||
* Run linters/syntax checkers
|
||||
|
||||
## Features
|
||||
|
||||
* Simple invocation and use
|
||||
* Runs on Linux, Mac, Windows, and more
|
||||
* Monitors current directory and all subdirectories for changes
|
||||
* Uses efficient event polling mechanism (on Linux, Mac, Windows, BSD)
|
||||
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
|
||||
* By default, uses `.gitignore`, `.ignore`, and other such files to determine which files to ignore notifications for
|
||||
* Support for watching files with a specific extension
|
||||
* Support for filtering/ignoring events based on [glob patterns](https://docs.rs/globset/*/globset/#syntax)
|
||||
* Launches the command in a new process group (can be disabled with `--no-process-group`)
|
||||
* Optionally clears screen between executions
|
||||
* Optionally restarts the command with every modification (good for servers)
|
||||
* Optionally sends a desktop notification on command start and end
|
||||
* Does not require a language runtime
|
||||
* Sets the following environment variables in the process:
|
||||
|
||||
`$WATCHEXEC_COMMON_PATH` is set to the longest common path of all of the below variables, and so should be prepended to each path to obtain the full/real path.
|
||||
|
||||
| Variable name | Event kind |
|
||||
|---|---|
|
||||
| `$WATCHEXEC_CREATED_PATH` | files/folders were created |
|
||||
| `$WATCHEXEC_REMOVED_PATH` | files/folders were removed |
|
||||
| `$WATCHEXEC_RENAMED_PATH` | files/folders were renamed |
|
||||
| `$WATCHEXEC_WRITTEN_PATH` | files/folders were modified |
|
||||
| `$WATCHEXEC_META_CHANGED_PATH` | files/folders' metadata were modified |
|
||||
| `$WATCHEXEC_OTHERWISE_CHANGED_PATH` | every other kind of event |
|
||||
|
||||
These variables may contain multiple paths: these are separated by the platform's path separator, as with the `PATH` system environment variable. On Unix that is `:`, and on Windows `;`. Within each variable, paths are deduplicated and sorted in binary order (i.e. neither Unicode nor locale aware).
|
||||
|
||||
This can be disabled with `--emit-events=none` or changed to JSON events on STDIN with `--emit-events=json-stdio`.
|
||||
|
||||
## Anti-Features
|
||||
|
||||
* Not tied to any particular language or ecosystem
|
||||
* Not tied to Git or the presence of a repository/project
|
||||
* Does not require a cryptic command line involving `xargs`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `make` when a change is detected:
|
||||
|
||||
$ watchexec --exts js,css,html make
|
||||
|
||||
Call `make test` when any file changes in this directory/subdirectory, except for everything below `target`:
|
||||
|
||||
$ watchexec -i "target/**" make test
|
||||
|
||||
Call `ls -la` when any file changes in this directory/subdirectory:
|
||||
|
||||
$ watchexec -- ls -la
|
||||
|
||||
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
|
||||
|
||||
$ watchexec -e py -r python server.py
|
||||
|
||||
Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the command:
|
||||
|
||||
$ watchexec -r --stop-signal SIGKILL my_server
|
||||
|
||||
Send a SIGHUP to the command upon changes (Note: using `-n` here we're executing `my_server` directly, instead of wrapping it in a shell:
|
||||
|
||||
$ watchexec -n --signal SIGHUP my_server
|
||||
|
||||
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
|
||||
|
||||
$ watchexec make
|
||||
|
||||
Run `make` when any file in `lib` or `src` changes:
|
||||
|
||||
$ watchexec -w lib -w src make
|
||||
|
||||
Run `bundle install` when the `Gemfile` changes:
|
||||
|
||||
$ watchexec -w Gemfile bundle install
|
||||
|
||||
Run two commands:
|
||||
|
||||
$ watchexec 'date; make'
|
||||
|
||||
Get desktop ("toast") notifications when the command starts and finishes:
|
||||
|
||||
$ watchexec -N go build
|
||||
|
||||
Only run when files are created:
|
||||
|
||||
$ watchexec --fs-events create -- s3 sync . s3://my-bucket
|
||||
|
||||
If you come from `entr`, note that the watchexec command is run in a shell by default. You can use `-n` or `--shell=none` to not do that:
|
||||
|
||||
$ watchexec -n -- echo ';' lorem ipsum
|
||||
|
||||
On Windows, you may prefer to use Powershell:
|
||||
|
||||
$ watchexec --shell=pwsh -- Test-Connection example.com
|
||||
|
||||
You can eschew running commands entirely and get a stream of events to process on your own:
|
||||
|
||||
```console
|
||||
$ watchexec --emit-events-to=json-stdio --only-emit-events
|
||||
|
||||
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/README.md","filetype":"file"}]}
|
||||
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/lib/Cargo.toml","filetype":"file"}]}
|
||||
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/src/args.rs","filetype":"file"}]}
|
||||
```
|
||||
|
||||
Print the time commands take to run:
|
||||
|
||||
```console
|
||||
$ watchexec --timings -- make
|
||||
[Running: make]
|
||||
...
|
||||
[Command was successful, lasted 52.748081074s]
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Package manager
|
||||
|
||||
Watchexec is in many package managers. A full list of [known packages](../../doc/packages.md) is available,
|
||||
and there may be more out there! Please contribute any you find to the list :)
|
||||
|
||||
Common package managers:
|
||||
|
||||
- Alpine: `$ apk add watchexec`
|
||||
- ArchLinux: `$ pacman -S watchexec`
|
||||
- Nix: `$ nix-shell -p watchexec`
|
||||
- Debian/Ubuntu via [apt.cli.rs](https://apt.cli.rs): `$ apt install watchexec`
|
||||
- Homebrew on Mac: `$ brew install watchexec`
|
||||
- Chocolatey on Windows: `#> choco install watchexec`
|
||||
|
||||
### [Binstall](https://github.com/cargo-bins/cargo-binstall)
|
||||
|
||||
$ cargo binstall watchexec-cli
|
||||
|
||||
### Pre-built binaries
|
||||
|
||||
Use the download section on [Github](https://github.com/watchexec/watchexec/releases/latest)
|
||||
or [the website](https://watchexec.github.io/downloads/) to obtain the package appropriate for your
|
||||
platform and architecture, extract it, and place it in your `PATH`.
|
||||
|
||||
There are also Debian/Ubuntu (DEB) and Fedora/RedHat (RPM) packages.
|
||||
|
||||
Checksums and signatures are available.
|
||||
|
||||
### Cargo (from source)
|
||||
|
||||
Only the latest Rust stable is supported, but older versions may work.
|
||||
|
||||
$ cargo install watchexec-cli
|
||||
|
||||
## Shell completions
|
||||
|
||||
Currently available shell completions:
|
||||
|
||||
- bash: `completions/bash` should be installed to `/usr/share/bash-completion/completions/watchexec`
|
||||
- elvish: `completions/elvish` should be installed to `$XDG_CONFIG_HOME/elvish/completions/`
|
||||
- fish: `completions/fish` should be installed to `/usr/share/fish/vendor_completions.d/watchexec.fish`
|
||||
- nu: `completions/nu` should be installed to `$XDG_CONFIG_HOME/nu/completions/`
|
||||
- powershell: `completions/powershell` should be installed to `$PROFILE/`
|
||||
- zsh: `completions/zsh` should be installed to `/usr/share/zsh/site-functions/_watchexec`
|
||||
|
||||
If not bundled, you can generate completions for your shell with `watchexec --completions <shell>`.
|
||||
|
||||
## Manual
|
||||
|
||||
There's a manual page at `doc/watchexec.1`. Install it to `/usr/share/man/man1/`.
|
||||
If not bundled, you can generate a manual page with `watchexec --manual > /path/to/watchexec.1`, or view it inline with `watchexec --manual` (requires `man`).
|
||||
|
||||
You can also [read a text version](../../doc/watchexec.1.md).
|
||||
|
||||
Note that it is automatically generated from the help text, so it is not as pretty as a carefully hand-written one.
|
||||
|
||||
## Advanced builds
|
||||
|
||||
These are additional options available with custom builds by setting features:
|
||||
|
||||
### PID1
|
||||
|
||||
If you're using Watchexec as PID1 (most frequently in containers or namespaces), and it's not doing what you expect, you can create a build with PID1 early logging: `--features pid1-withlog`.
|
||||
|
||||
If you don't need PID1 support, or if you're doing something that conflicts with this program's PID1 support, you can disable it with `--no-default-features`.
|
||||
|
||||
### Eyra
|
||||
|
||||
[Eyra](https://github.com/sunfishcode/eyra) is a system to build Linux programs with no dependency on C code (in the libc path). To build Watchexec like this, use `--features eyra` and a Nightly compiler.
|
||||
|
||||
This feature also lets you get early logging into program startup, with `RUST_LOG=trace`.
|
8
crates/cli/build.rs
Normal file
8
crates/cli/build.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
fn main() {
|
||||
embed_resource::compile("watchexec-manifest.rc", embed_resource::NONE);
|
||||
bosion::gather();
|
||||
|
||||
if std::env::var("CARGO_FEATURE_EYRA").is_ok() {
|
||||
println!("cargo:rustc-link-arg=-nostartfiles");
|
||||
}
|
||||
}
|
7
crates/cli/integration/env.sh
Executable file
7
crates/cli/integration/env.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
watchexec=${WATCHEXEC_BIN:-watchexec}
|
||||
|
||||
$watchexec -1 --env FOO=BAR echo '$FOO' | grep BAR
|
7
crates/cli/integration/no-shell.sh
Executable file
7
crates/cli/integration/no-shell.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
watchexec=${WATCHEXEC_BIN:-watchexec}
|
||||
|
||||
$watchexec -1 -n echo 'foo bar' | grep 'foo bar'
|
7
crates/cli/integration/stdin-quit.sh
Executable file
7
crates/cli/integration/stdin-quit.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
watchexec=${WATCHEXEC_BIN:-watchexec}
|
||||
|
||||
timeout -s9 30s sh -c "sleep 10 | $watchexec --stdin-quit echo"
|
7
crates/cli/integration/trailingargfile.sh
Executable file
7
crates/cli/integration/trailingargfile.sh
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
watchexec=${WATCHEXEC_BIN:-watchexec}
|
||||
|
||||
$watchexec -1 -- echo @trailingargfile
|
26
crates/cli/release.toml
Normal file
26
crates/cli/release.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
pre-release-commit-message = "release: cli v{{version}}"
|
||||
tag-prefix = ""
|
||||
tag-message = "watchexec {{version}}"
|
||||
|
||||
pre-release-hook = ["sh", "-c", "cd ../.. && bin/completions && bin/manpage"]
|
||||
|
||||
[[pre-release-replacements]]
|
||||
file = "watchexec.exe.manifest"
|
||||
search = "^ version=\"[\\d.]+[.]0\""
|
||||
replace = " version=\"{{version}}.0\""
|
||||
prerelease = false
|
||||
max = 1
|
||||
|
||||
[[pre-release-replacements]]
|
||||
file = "../../CITATION.cff"
|
||||
search = "^version: \"?[\\d.]+(-.+)?\"?"
|
||||
replace = "version: \"{{version}}\""
|
||||
prerelease = true
|
||||
max = 1
|
||||
|
||||
[[pre-release-replacements]]
|
||||
file = "../../CITATION.cff"
|
||||
search = "^date-released: .+"
|
||||
replace = "date-released: {{date}}"
|
||||
prerelease = true
|
||||
max = 1
|
13
crates/cli/run-tests.sh
Executable file
13
crates/cli/run-tests.sh
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export WATCHEXEC_BIN=$(realpath ${WATCHEXEC_BIN:-$(which watchexec)})
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/integration"
|
||||
for test in *.sh; do
|
||||
echo
|
||||
echo
|
||||
echo "======= Testing $test ======="
|
||||
./$test
|
||||
done
|
1344
crates/cli/src/args.rs
Normal file
1344
crates/cli/src/args.rs
Normal file
File diff suppressed because it is too large
Load diff
132
crates/cli/src/args/logging.rs
Normal file
132
crates/cli/src/args/logging.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use std::{env::var, io::stderr, path::PathBuf};
|
||||
|
||||
use clap::{ArgAction, Parser, ValueHint};
|
||||
use miette::{bail, Result};
|
||||
use tokio::fs::metadata;
|
||||
use tracing::{info, warn};
|
||||
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct LoggingArgs {
|
||||
/// Set diagnostic log level
|
||||
///
|
||||
/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
|
||||
/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
|
||||
///
|
||||
/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
|
||||
///
|
||||
/// You may want to use with '--log-file' to avoid polluting your terminal.
|
||||
///
|
||||
/// Setting $RUST_LOG also works, and takes precedence, but is not recommended. However, using
|
||||
/// $RUST_LOG is the only way to get logs from before these options are parsed.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help_heading = super::OPTSET_DEBUGGING,
|
||||
action = ArgAction::Count,
|
||||
default_value = "0",
|
||||
num_args = 0,
|
||||
)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Write diagnostic logs to a file
|
||||
///
|
||||
/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
|
||||
/// level was not already specified, this will set it to '-vvv'.
|
||||
///
|
||||
/// If a path is not provided, the default is the working directory. Note that with
|
||||
/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
|
||||
/// causing a loop; prefer setting a path outside of the watched directory.
|
||||
///
|
||||
/// If the path provided is a directory, a file will be created in that directory. The file name
|
||||
/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
|
||||
#[arg(
|
||||
long,
|
||||
help_heading = super::OPTSET_DEBUGGING,
|
||||
num_args = 0..=1,
|
||||
default_missing_value = ".",
|
||||
value_hint = ValueHint::AnyPath,
|
||||
value_name = "PATH",
|
||||
)]
|
||||
pub log_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn preargs() -> bool {
|
||||
let mut log_on = false;
|
||||
|
||||
#[cfg(feature = "dev-console")]
|
||||
match console_subscriber::try_init() {
|
||||
Ok(_) => {
|
||||
warn!("dev-console enabled");
|
||||
log_on = true;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
|
||||
}
|
||||
}
|
||||
|
||||
if !log_on && var("RUST_LOG").is_ok() {
|
||||
match tracing_subscriber::fmt::try_init() {
|
||||
Ok(()) => {
|
||||
warn!(RUST_LOG=%var("RUST_LOG").unwrap(), "logging configured from RUST_LOG");
|
||||
log_on = true;
|
||||
}
|
||||
Err(e) => eprintln!("Failed to initialise logging with RUST_LOG, falling back\n{e}"),
|
||||
}
|
||||
}
|
||||
|
||||
log_on
|
||||
}
|
||||
|
||||
pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
|
||||
if args.verbose == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (log_writer, guard) = if let Some(file) = &args.log_file {
|
||||
let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
|
||||
let (dir, filename) = if is_dir {
|
||||
(
|
||||
file.to_owned(),
|
||||
PathBuf::from(format!(
|
||||
"watchexec.{}.log",
|
||||
chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
|
||||
)),
|
||||
)
|
||||
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
|
||||
(parent.into(), PathBuf::from(file_name))
|
||||
} else {
|
||||
bail!("Failed to determine log file name");
|
||||
};
|
||||
|
||||
non_blocking(rolling::never(dir, filename))
|
||||
} else {
|
||||
non_blocking(stderr())
|
||||
};
|
||||
|
||||
let mut builder = tracing_subscriber::fmt().with_env_filter(match args.verbose {
|
||||
0 => unreachable!("checked by if earlier"),
|
||||
1 => "warn",
|
||||
2 => "info",
|
||||
3 => "debug",
|
||||
_ => "trace",
|
||||
});
|
||||
|
||||
if args.verbose > 2 {
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
|
||||
}
|
||||
|
||||
match if args.log_file.is_some() {
|
||||
builder.json().with_writer(log_writer).try_init()
|
||||
} else if args.verbose > 3 {
|
||||
builder.pretty().with_writer(log_writer).try_init()
|
||||
} else {
|
||||
builder.with_writer(log_writer).try_init()
|
||||
} {
|
||||
Ok(()) => info!("logging initialised"),
|
||||
Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
|
||||
}
|
||||
|
||||
Ok(Some(guard))
|
||||
}
|
708
crates/cli/src/config.rs
Normal file
708
crates/cli/src/config.rs
Normal file
|
@ -0,0 +1,708 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
env::var,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::File,
|
||||
io::{IsTerminal, Write},
|
||||
process::Stdio,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use clearscreen::ClearScreen;
|
||||
use miette::{miette, IntoDiagnostic, Report, Result};
|
||||
use notify_rust::Notification;
|
||||
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
|
||||
use tokio::{process::Command as TokioCommand, time::sleep};
|
||||
use tracing::{debug, debug_span, error, instrument, trace, trace_span, Instrument};
|
||||
use watchexec::{
|
||||
action::ActionHandler,
|
||||
command::{Command, Program, Shell, SpawnOptions},
|
||||
error::RuntimeError,
|
||||
job::{CommandState, Job},
|
||||
sources::fs::Watcher,
|
||||
Config, ErrorHook, Id,
|
||||
};
|
||||
use watchexec_events::{Event, Keyboard, ProcessEnd, Tag};
|
||||
use watchexec_signals::Signal;
|
||||
|
||||
use crate::{
|
||||
args::{Args, ClearMode, ColourMode, EmitEvents, OnBusyUpdate, SignalMapping, WrapMode},
|
||||
state::RotatingTempFile,
|
||||
};
|
||||
use crate::{emits::events_to_simple_format, state::State};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct OutputFlags {
|
||||
quiet: bool,
|
||||
colour: ColorChoice,
|
||||
timings: bool,
|
||||
bell: bool,
|
||||
toast: bool,
|
||||
}
|
||||
|
||||
pub fn make_config(args: &Args, state: &State) -> Result<Config> {
|
||||
let _span = debug_span!("args-runtime").entered();
|
||||
let config = Config::default();
|
||||
config.on_error(|err: ErrorHook| {
|
||||
if let RuntimeError::IoError {
|
||||
about: "waiting on process group",
|
||||
..
|
||||
} = err.error
|
||||
{
|
||||
// "No child processes" and such
|
||||
// these are often spurious, so condemn them to -v only
|
||||
error!("{}", err.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[[{:?}]]", err.error);
|
||||
}
|
||||
|
||||
eprintln!("[[Error (not fatal)]]\n{}", Report::new(err.error));
|
||||
});
|
||||
|
||||
config.pathset(args.paths.clone());
|
||||
|
||||
config.throttle(args.debounce.0);
|
||||
config.keyboard_events(args.stdin_quit);
|
||||
|
||||
if let Some(interval) = args.poll {
|
||||
config.file_watcher(Watcher::Poll(interval.0));
|
||||
}
|
||||
|
||||
let once = args.once;
|
||||
let clear = args.screen_clear;
|
||||
|
||||
let emit_events_to = args.emit_events_to;
|
||||
let emit_file = state.emit_file.clone();
|
||||
|
||||
if args.only_emit_events {
|
||||
config.on_action(move |mut action| {
|
||||
// if we got a terminate or interrupt signal, quit
|
||||
if action
|
||||
.signals()
|
||||
.any(|sig| sig == Signal::Terminate || sig == Signal::Interrupt)
|
||||
{
|
||||
// no need to be graceful as there's no commands
|
||||
action.quit();
|
||||
return action;
|
||||
}
|
||||
|
||||
// clear the screen before printing events
|
||||
if let Some(mode) = clear {
|
||||
match mode {
|
||||
ClearMode::Clear => {
|
||||
clearscreen::clear().ok();
|
||||
}
|
||||
ClearMode::Reset => {
|
||||
reset_screen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match emit_events_to {
|
||||
EmitEvents::Stdio => {
|
||||
println!(
|
||||
"{}",
|
||||
events_to_simple_format(action.events.as_ref()).unwrap_or_default()
|
||||
);
|
||||
}
|
||||
EmitEvents::JsonStdio => {
|
||||
for event in action.events.iter().filter(|e| !e.is_empty()) {
|
||||
println!("{}", serde_json::to_string(event).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
other => unreachable!(
|
||||
"emit_events_to should have been validated earlier: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
|
||||
action
|
||||
});
|
||||
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let delay_run = args.delay_run.map(|ts| ts.0);
|
||||
let on_busy = args.on_busy_update;
|
||||
let stdin_quit = args.stdin_quit;
|
||||
|
||||
let signal = args.signal;
|
||||
let stop_signal = args.stop_signal;
|
||||
let stop_timeout = args.stop_timeout.0;
|
||||
|
||||
let print_events = args.print_events;
|
||||
let outflags = OutputFlags {
|
||||
quiet: args.quiet,
|
||||
colour: match args.color {
|
||||
ColourMode::Auto if !std::io::stdin().is_terminal() => ColorChoice::Never,
|
||||
ColourMode::Auto => ColorChoice::Auto,
|
||||
ColourMode::Always => ColorChoice::Always,
|
||||
ColourMode::Never => ColorChoice::Never,
|
||||
},
|
||||
timings: args.timings,
|
||||
bell: args.bell,
|
||||
toast: args.notify,
|
||||
};
|
||||
|
||||
let workdir = Arc::new(args.workdir.clone());
|
||||
|
||||
let mut add_envs = HashMap::new();
|
||||
for pair in &args.env {
|
||||
if let Some((k, v)) = pair.split_once('=') {
|
||||
add_envs.insert(k.to_owned(), OsString::from(v));
|
||||
} else {
|
||||
return Err(miette!("{pair} is not in key=value format"));
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
?add_envs,
|
||||
"additional environment variables to add to command"
|
||||
);
|
||||
|
||||
let id = Id::default();
|
||||
let command = interpret_command_args(args)?;
|
||||
|
||||
let signal_map: Arc<HashMap<Signal, Option<Signal>>> = Arc::new(
|
||||
args.signal_map
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|SignalMapping { from, to }| (from, to))
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let queued = Arc::new(AtomicBool::new(false));
|
||||
let quit_again = Arc::new(AtomicU8::new(0));
|
||||
|
||||
config.on_action_async(move |mut action| {
|
||||
let add_envs = add_envs.clone();
|
||||
let command = command.clone();
|
||||
let emit_file = emit_file.clone();
|
||||
let queued = queued.clone();
|
||||
let quit_again = quit_again.clone();
|
||||
let signal_map = signal_map.clone();
|
||||
let workdir = workdir.clone();
|
||||
Box::new(
|
||||
async move {
|
||||
trace!(events=?action.events, "handling action");
|
||||
|
||||
let add_envs = add_envs.clone();
|
||||
let command = command.clone();
|
||||
let emit_file = emit_file.clone();
|
||||
let queued = queued.clone();
|
||||
let quit_again = quit_again.clone();
|
||||
let signal_map = signal_map.clone();
|
||||
let workdir = workdir.clone();
|
||||
|
||||
trace!("set spawn hook for workdir and environment variables");
|
||||
let job = action.get_or_create_job(id, move || command.clone());
|
||||
let events = action.events.clone();
|
||||
job.set_spawn_hook(move |command, _| {
|
||||
let add_envs = add_envs.clone();
|
||||
let emit_file = emit_file.clone();
|
||||
let events = events.clone();
|
||||
|
||||
if let Some(ref workdir) = workdir.as_ref() {
|
||||
debug!(?workdir, "set command workdir");
|
||||
command.command_mut().current_dir(workdir);
|
||||
}
|
||||
|
||||
emit_events_to_command(
|
||||
command.command_mut(),
|
||||
events,
|
||||
emit_file,
|
||||
emit_events_to,
|
||||
add_envs,
|
||||
);
|
||||
});
|
||||
|
||||
let show_events = {
|
||||
let events = action.events.clone();
|
||||
move || {
|
||||
if print_events {
|
||||
trace!("print events to stderr");
|
||||
for (n, event) in events.iter().enumerate() {
|
||||
eprintln!("[EVENT {n}] {event}");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let clear_screen = {
|
||||
let events = action.events.clone();
|
||||
move || {
|
||||
if let Some(mode) = clear {
|
||||
match mode {
|
||||
ClearMode::Clear => {
|
||||
clearscreen::clear().ok();
|
||||
debug!("cleared screen");
|
||||
}
|
||||
ClearMode::Reset => {
|
||||
reset_screen();
|
||||
debug!("hard-reset screen");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// re-show events after clearing
|
||||
if print_events {
|
||||
trace!("print events to stderr");
|
||||
for (n, event) in events.iter().enumerate() {
|
||||
eprintln!("[EVENT {n}] {event}");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let quit = |mut action: ActionHandler| {
|
||||
match quit_again.fetch_add(1, Ordering::Relaxed) {
|
||||
0 => {
|
||||
eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping...]");
|
||||
// eprintln!("[Waiting {stop_timeout:?} for processes to exit before stopping... Ctrl-C again to exit faster]");
|
||||
// see TODO in action/worker.rs
|
||||
action.quit_gracefully(
|
||||
stop_signal.unwrap_or(Signal::Terminate),
|
||||
stop_timeout,
|
||||
);
|
||||
}
|
||||
1 => {
|
||||
action.quit_gracefully(Signal::ForceStop, Duration::ZERO);
|
||||
}
|
||||
_ => {
|
||||
action.quit();
|
||||
}
|
||||
}
|
||||
|
||||
action
|
||||
};
|
||||
|
||||
if once {
|
||||
debug!("debug mode: run once and quit");
|
||||
show_events();
|
||||
|
||||
if let Some(delay) = delay_run {
|
||||
job.run_async(move |_| {
|
||||
Box::new(async move {
|
||||
sleep(delay).await;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// this blocks the event loop, but also this is a debug feature so i don't care
|
||||
job.start().await;
|
||||
job.to_wait().await;
|
||||
return quit(action);
|
||||
}
|
||||
|
||||
let is_keyboard_eof = action
|
||||
.events
|
||||
.iter()
|
||||
.any(|e| e.tags.contains(&Tag::Keyboard(Keyboard::Eof)));
|
||||
if stdin_quit && is_keyboard_eof {
|
||||
debug!("keyboard EOF, quit");
|
||||
show_events();
|
||||
return quit(action);
|
||||
}
|
||||
|
||||
let signals: Vec<Signal> = action.signals().collect();
|
||||
trace!(?signals, "received some signals");
|
||||
|
||||
// if we got a terminate or interrupt signal and they're not mapped, quit
|
||||
if (signals.contains(&Signal::Terminate)
|
||||
&& !signal_map.contains_key(&Signal::Terminate))
|
||||
|| (signals.contains(&Signal::Interrupt)
|
||||
&& !signal_map.contains_key(&Signal::Interrupt))
|
||||
{
|
||||
debug!("unmapped terminate or interrupt signal, quit");
|
||||
show_events();
|
||||
return quit(action);
|
||||
}
|
||||
|
||||
// pass all other signals on
|
||||
for signal in signals {
|
||||
match signal_map.get(&signal) {
|
||||
Some(Some(mapped)) => {
|
||||
debug!(?signal, ?mapped, "passing mapped signal");
|
||||
job.signal(*mapped);
|
||||
}
|
||||
Some(None) => {
|
||||
debug!(?signal, "discarding signal");
|
||||
}
|
||||
None => {
|
||||
debug!(?signal, "passing signal on");
|
||||
job.signal(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only filesystem events below here (or empty synthetic events)
|
||||
if action.paths().next().is_none() && !action.events.iter().any(|e| e.is_empty()) {
|
||||
debug!("no filesystem or synthetic events, skip without doing more");
|
||||
show_events();
|
||||
return action;
|
||||
}
|
||||
|
||||
show_events();
|
||||
|
||||
if let Some(delay) = delay_run {
|
||||
trace!("delaying run by sleeping inside the job");
|
||||
job.run_async(move |_| {
|
||||
Box::new(async move {
|
||||
sleep(delay).await;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
trace!("querying job state via run_async");
|
||||
job.run_async({
|
||||
let job = job.clone();
|
||||
move |context| {
|
||||
let job = job.clone();
|
||||
let is_running = matches!(context.current, CommandState::Running { .. });
|
||||
Box::new(async move {
|
||||
let innerjob = job.clone();
|
||||
if is_running {
|
||||
trace!(?on_busy, "job is running, decide what to do");
|
||||
match on_busy {
|
||||
OnBusyUpdate::DoNothing => {}
|
||||
OnBusyUpdate::Signal => {
|
||||
job.signal(if cfg!(windows) {
|
||||
Signal::ForceStop
|
||||
} else {
|
||||
stop_signal.or(signal).unwrap_or(Signal::Terminate)
|
||||
});
|
||||
}
|
||||
OnBusyUpdate::Restart if cfg!(windows) => {
|
||||
job.restart();
|
||||
job.run(move |context| {
|
||||
clear_screen();
|
||||
setup_process(
|
||||
innerjob.clone(),
|
||||
context.command.clone(),
|
||||
outflags,
|
||||
)
|
||||
});
|
||||
}
|
||||
OnBusyUpdate::Restart => {
|
||||
job.restart_with_signal(
|
||||
stop_signal.unwrap_or(Signal::Terminate),
|
||||
stop_timeout,
|
||||
);
|
||||
job.run(move |context| {
|
||||
clear_screen();
|
||||
setup_process(
|
||||
innerjob.clone(),
|
||||
context.command.clone(),
|
||||
outflags,
|
||||
)
|
||||
});
|
||||
}
|
||||
OnBusyUpdate::Queue => {
|
||||
let job = job.clone();
|
||||
let already_queued =
|
||||
queued.fetch_or(true, Ordering::SeqCst);
|
||||
if already_queued {
|
||||
debug!("next start is already queued, do nothing");
|
||||
} else {
|
||||
debug!("queueing next start of job");
|
||||
tokio::spawn({
|
||||
let queued = queued.clone();
|
||||
async move {
|
||||
trace!("waiting for job to finish");
|
||||
job.to_wait().await;
|
||||
trace!("job finished, starting queued");
|
||||
job.start();
|
||||
job.run(move |context| {
|
||||
clear_screen();
|
||||
setup_process(
|
||||
innerjob.clone(),
|
||||
context.command.clone(),
|
||||
outflags,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
trace!("resetting queued state");
|
||||
queued.store(false, Ordering::SeqCst);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trace!("job is not running, start it");
|
||||
job.start();
|
||||
job.run(move |context| {
|
||||
clear_screen();
|
||||
setup_process(
|
||||
innerjob.clone(),
|
||||
context.command.clone(),
|
||||
outflags,
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
action
|
||||
}
|
||||
.instrument(trace_span!("action handler")),
|
||||
)
|
||||
});
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[instrument(level = "debug")]
|
||||
fn interpret_command_args(args: &Args) -> Result<Arc<Command>> {
|
||||
let mut cmd = args.command.clone();
|
||||
if cmd.is_empty() {
|
||||
panic!("(clap) Bug: command is not present");
|
||||
}
|
||||
|
||||
let shell = if args.no_shell {
|
||||
None
|
||||
} else {
|
||||
let shell = args.shell.clone().or_else(|| var("SHELL").ok());
|
||||
match shell
|
||||
.as_deref()
|
||||
.or_else(|| {
|
||||
if cfg!(not(windows)) {
|
||||
Some("sh")
|
||||
} else if var("POWERSHELL_DISTRIBUTION_CHANNEL").is_ok()
|
||||
&& (which::which("pwsh").is_ok() || which::which("pwsh.exe").is_ok())
|
||||
{
|
||||
trace!("detected pwsh");
|
||||
Some("pwsh")
|
||||
} else if var("PSModulePath").is_ok()
|
||||
&& (which::which("powershell").is_ok()
|
||||
|| which::which("powershell.exe").is_ok())
|
||||
{
|
||||
trace!("detected powershell");
|
||||
Some("powershell")
|
||||
} else {
|
||||
Some("cmd")
|
||||
}
|
||||
})
|
||||
.or(Some("default"))
|
||||
{
|
||||
Some("") => return Err(RuntimeError::CommandShellEmptyShell).into_diagnostic(),
|
||||
|
||||
Some("none") | None => None,
|
||||
|
||||
#[cfg(windows)]
|
||||
Some("cmd") | Some("cmd.exe") | Some("CMD") | Some("CMD.EXE") => Some(Shell::cmd()),
|
||||
|
||||
Some(other) => {
|
||||
let sh = other.split_ascii_whitespace().collect::<Vec<_>>();
|
||||
|
||||
// UNWRAP: checked by Some("")
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let (shprog, shopts) = sh.split_first().unwrap();
|
||||
|
||||
Some(Shell {
|
||||
prog: shprog.into(),
|
||||
options: shopts.iter().map(|s| (*s).to_string()).collect(),
|
||||
program_option: Some(Cow::Borrowed(OsStr::new("-c"))),
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let program = if let Some(shell) = shell {
|
||||
Program::Shell {
|
||||
shell,
|
||||
command: cmd.join(" "),
|
||||
args: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
Program::Exec {
|
||||
prog: cmd.remove(0).into(),
|
||||
args: cmd,
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Arc::new(Command {
|
||||
program,
|
||||
options: SpawnOptions {
|
||||
grouped: matches!(args.wrap_process, WrapMode::Group),
|
||||
session: matches!(args.wrap_process, WrapMode::Session),
|
||||
..Default::default()
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[instrument(level = "trace")]
|
||||
fn setup_process(job: Job, command: Arc<Command>, outflags: OutputFlags) {
|
||||
if outflags.toast {
|
||||
Notification::new()
|
||||
.summary("Watchexec: change detected")
|
||||
.body(&format!("Running {command}"))
|
||||
.show()
|
||||
.map_or_else(
|
||||
|err| {
|
||||
eprintln!("[[Failed to send desktop notification: {err}]]");
|
||||
},
|
||||
drop,
|
||||
);
|
||||
}
|
||||
|
||||
if !outflags.quiet {
|
||||
let mut stderr = StandardStream::stderr(outflags.colour);
|
||||
stderr.reset().ok();
|
||||
stderr
|
||||
.set_color(ColorSpec::new().set_fg(Some(Color::Green)))
|
||||
.ok();
|
||||
writeln!(&mut stderr, "[Running: {command}]").ok();
|
||||
stderr.reset().ok();
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
job.to_wait().await;
|
||||
job.run(move |context| end_of_process(context.current, outflags));
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(level = "trace")]
|
||||
fn end_of_process(state: &CommandState, outflags: OutputFlags) {
|
||||
let CommandState::Finished {
|
||||
status,
|
||||
started,
|
||||
finished,
|
||||
} = state
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let duration = *finished - *started;
|
||||
let timing = if outflags.timings {
|
||||
format!(", lasted {duration:?}")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let (msg, fg) = match status {
|
||||
ProcessEnd::ExitError(code) => (format!("Command exited with {code}{timing}"), Color::Red),
|
||||
ProcessEnd::ExitSignal(sig) => {
|
||||
(format!("Command killed by {sig:?}{timing}"), Color::Magenta)
|
||||
}
|
||||
ProcessEnd::ExitStop(sig) => (format!("Command stopped by {sig:?}{timing}"), Color::Blue),
|
||||
ProcessEnd::Continued => (format!("Command continued{timing}"), Color::Cyan),
|
||||
ProcessEnd::Exception(ex) => (
|
||||
format!("Command ended by exception {ex:#x}{timing}"),
|
||||
Color::Yellow,
|
||||
),
|
||||
ProcessEnd::Success => (format!("Command was successful{timing}"), Color::Green),
|
||||
};
|
||||
|
||||
if outflags.toast {
|
||||
Notification::new()
|
||||
.summary("Watchexec: command ended")
|
||||
.body(&msg)
|
||||
.show()
|
||||
.map_or_else(
|
||||
|err| {
|
||||
eprintln!("[[Failed to send desktop notification: {err}]]");
|
||||
},
|
||||
drop,
|
||||
);
|
||||
}
|
||||
|
||||
if !outflags.quiet {
|
||||
let mut stderr = StandardStream::stderr(outflags.colour);
|
||||
stderr.reset().ok();
|
||||
stderr.set_color(ColorSpec::new().set_fg(Some(fg))).ok();
|
||||
writeln!(&mut stderr, "[{msg}]").ok();
|
||||
stderr.reset().ok();
|
||||
}
|
||||
|
||||
if outflags.bell {
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(b"\x07").ok();
|
||||
stdout.flush().ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "trace")]
|
||||
fn emit_events_to_command(
|
||||
command: &mut TokioCommand,
|
||||
events: Arc<[Event]>,
|
||||
emit_file: RotatingTempFile,
|
||||
emit_events_to: EmitEvents,
|
||||
mut add_envs: HashMap<String, OsString>,
|
||||
) {
|
||||
use crate::emits::*;
|
||||
|
||||
let mut stdin = None;
|
||||
|
||||
match emit_events_to {
|
||||
EmitEvents::Environment => {
|
||||
add_envs.extend(emits_to_environment(&events));
|
||||
}
|
||||
EmitEvents::Stdio => match emits_to_file(&emit_file, &events)
|
||||
.and_then(|path| File::open(path).into_diagnostic())
|
||||
{
|
||||
Ok(file) => {
|
||||
stdin.replace(Stdio::from(file));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to write events to stdin, continuing without it: {err}");
|
||||
}
|
||||
},
|
||||
EmitEvents::File => match emits_to_file(&emit_file, &events) {
|
||||
Ok(path) => {
|
||||
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
|
||||
}
|
||||
},
|
||||
EmitEvents::JsonStdio => match emits_to_json_file(&emit_file, &events)
|
||||
.and_then(|path| File::open(path).into_diagnostic())
|
||||
{
|
||||
Ok(file) => {
|
||||
stdin.replace(Stdio::from(file));
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to write events to stdin, continuing without it: {err}");
|
||||
}
|
||||
},
|
||||
EmitEvents::JsonFile => match emits_to_json_file(&emit_file, &events) {
|
||||
Ok(path) => {
|
||||
add_envs.insert("WATCHEXEC_EVENTS_FILE".into(), path.into());
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to write WATCHEXEC_EVENTS_FILE, continuing without it: {err}");
|
||||
}
|
||||
},
|
||||
EmitEvents::None => {}
|
||||
}
|
||||
|
||||
for (k, v) in add_envs {
|
||||
debug!(?k, ?v, "inserting environment variable");
|
||||
command.env(k, v);
|
||||
}
|
||||
|
||||
if let Some(stdin) = stdin {
|
||||
debug!("set command stdin");
|
||||
command.stdin(stdin);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reset_screen() {
|
||||
for cs in [
|
||||
ClearScreen::WindowsCooked,
|
||||
ClearScreen::WindowsVt,
|
||||
ClearScreen::VtLeaveAlt,
|
||||
ClearScreen::VtWellDone,
|
||||
ClearScreen::default(),
|
||||
] {
|
||||
cs.clear().ok();
|
||||
}
|
||||
}
|
222
crates/cli/src/dirs.rs
Normal file
222
crates/cli/src/dirs.rs
Normal file
|
@ -0,0 +1,222 @@
|
|||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use ignore_files::{IgnoreFile, IgnoreFilesFromOriginArgs};
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use project_origins::ProjectType;
|
||||
use tokio::fs::canonicalize;
|
||||
use tracing::{debug, info, warn};
|
||||
use watchexec::paths::common_prefix;
|
||||
|
||||
use crate::args::Args;
|
||||
|
||||
pub async fn project_origin(args: &Args) -> Result<PathBuf> {
|
||||
let project_origin = if let Some(origin) = &args.project_origin {
|
||||
debug!(?origin, "project origin override");
|
||||
canonicalize(origin).await.into_diagnostic()?
|
||||
} else {
|
||||
let homedir = match dirs::home_dir() {
|
||||
None => None,
|
||||
Some(dir) => Some(canonicalize(dir).await.into_diagnostic()?),
|
||||
};
|
||||
debug!(?homedir, "home directory");
|
||||
|
||||
let homedir_requested = homedir.as_ref().map_or(false, |home| {
|
||||
args.paths
|
||||
.binary_search_by_key(home, |w| PathBuf::from(w.clone()))
|
||||
.is_ok()
|
||||
});
|
||||
debug!(
|
||||
?homedir_requested,
|
||||
"resolved whether the homedir is explicitly requested"
|
||||
);
|
||||
|
||||
let mut origins = HashSet::new();
|
||||
for path in &args.paths {
|
||||
origins.extend(project_origins::origins(path).await);
|
||||
}
|
||||
|
||||
match (homedir, homedir_requested) {
|
||||
(Some(ref dir), false) if origins.contains(dir) => {
|
||||
debug!("removing homedir from origins");
|
||||
origins.remove(dir);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if origins.is_empty() {
|
||||
debug!("no origins, using current directory");
|
||||
origins.insert(args.workdir.clone().unwrap());
|
||||
}
|
||||
|
||||
debug!(?origins, "resolved all project origins");
|
||||
|
||||
// This canonicalize is probably redundant
|
||||
canonicalize(
|
||||
common_prefix(&origins)
|
||||
.ok_or_else(|| miette!("no common prefix, but this should never fail"))?,
|
||||
)
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
};
|
||||
debug!(?project_origin, "resolved common/project origin");
|
||||
|
||||
Ok(project_origin)
|
||||
}
|
||||
|
||||
pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
|
||||
let vcs_types = project_origins::types(origin)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|pt| pt.is_vcs())
|
||||
.collect::<Vec<_>>();
|
||||
info!(?vcs_types, "effective vcs types");
|
||||
vcs_types
|
||||
}
|
||||
|
||||
pub async fn ignores(args: &Args, vcs_types: &[ProjectType]) -> Result<Vec<IgnoreFile>> {
|
||||
let origin = args.project_origin.clone().unwrap();
|
||||
let mut skip_git_global_excludes = false;
|
||||
|
||||
let mut ignores = if args.no_project_ignore {
|
||||
Vec::new()
|
||||
} else {
|
||||
let ignore_files = args.ignore_files.iter().map(|path| {
|
||||
if path.is_absolute() {
|
||||
path.into()
|
||||
} else {
|
||||
origin.join(path)
|
||||
}
|
||||
});
|
||||
|
||||
let (mut ignores, errors) = ignore_files::from_origin(
|
||||
IgnoreFilesFromOriginArgs::new_unchecked(
|
||||
&origin,
|
||||
args.paths.iter().map(PathBuf::from),
|
||||
ignore_files,
|
||||
)
|
||||
.canonicalise()
|
||||
.await
|
||||
.into_diagnostic()?,
|
||||
)
|
||||
.await;
|
||||
|
||||
for err in errors {
|
||||
warn!("while discovering project-local ignore files: {}", err);
|
||||
}
|
||||
debug!(?ignores, "discovered ignore files from project origin");
|
||||
|
||||
if !vcs_types.is_empty() {
|
||||
ignores = ignores
|
||||
.into_iter()
|
||||
.filter(|ig| match ig.applies_to {
|
||||
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
|
||||
_ => true,
|
||||
})
|
||||
.inspect(|ig| {
|
||||
if let IgnoreFile {
|
||||
applies_to: Some(ProjectType::Git),
|
||||
applies_in: None,
|
||||
..
|
||||
} = ig
|
||||
{
|
||||
warn!("project git config overrides the global excludes");
|
||||
skip_git_global_excludes = true;
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
debug!(?ignores, "filtered ignores to only those for project vcs");
|
||||
}
|
||||
|
||||
ignores
|
||||
};
|
||||
|
||||
let global_ignores = if args.no_global_ignore {
|
||||
Vec::new()
|
||||
} else {
|
||||
let (mut global_ignores, errors) = ignore_files::from_environment(Some("watchexec")).await;
|
||||
for err in errors {
|
||||
warn!("while discovering global ignore files: {}", err);
|
||||
}
|
||||
debug!(?global_ignores, "discovered ignore files from environment");
|
||||
|
||||
if skip_git_global_excludes {
|
||||
global_ignores = global_ignores
|
||||
.into_iter()
|
||||
.filter(|gig| {
|
||||
!matches!(
|
||||
gig,
|
||||
IgnoreFile {
|
||||
applies_to: Some(ProjectType::Git),
|
||||
applies_in: None,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
debug!(
|
||||
?global_ignores,
|
||||
"filtered global ignores to exclude global git ignores"
|
||||
);
|
||||
}
|
||||
|
||||
global_ignores
|
||||
};
|
||||
|
||||
ignores.extend(global_ignores.into_iter().filter(|ig| match ig.applies_to {
|
||||
Some(pt) if pt.is_vcs() => vcs_types.contains(&pt),
|
||||
_ => true,
|
||||
}));
|
||||
debug!(
|
||||
?ignores,
|
||||
?vcs_types,
|
||||
"combined and applied overall vcs filter over ignores"
|
||||
);
|
||||
|
||||
ignores.extend(args.ignore_files.iter().map(|ig| IgnoreFile {
|
||||
applies_to: None,
|
||||
applies_in: None,
|
||||
path: ig.clone(),
|
||||
}));
|
||||
debug!(
|
||||
?ignores,
|
||||
?args.ignore_files,
|
||||
"combined with ignore files from command line / env"
|
||||
);
|
||||
|
||||
if args.no_project_ignore {
|
||||
ignores = ignores
|
||||
.into_iter()
|
||||
.filter(|ig| {
|
||||
!ig.applies_in
|
||||
.as_ref()
|
||||
.map_or(false, |p| p.starts_with(&origin))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
debug!(
|
||||
?ignores,
|
||||
"filtered ignores to exclude project-local ignores"
|
||||
);
|
||||
}
|
||||
|
||||
if args.no_global_ignore {
|
||||
ignores = ignores
|
||||
.into_iter()
|
||||
.filter(|ig| ig.applies_in.is_some())
|
||||
.collect::<Vec<_>>();
|
||||
debug!(?ignores, "filtered ignores to exclude global ignores");
|
||||
}
|
||||
|
||||
if args.no_vcs_ignore {
|
||||
ignores = ignores
|
||||
.into_iter()
|
||||
.filter(|ig| ig.applies_to.is_none())
|
||||
.collect::<Vec<_>>();
|
||||
debug!(?ignores, "filtered ignores to exclude VCS-specific ignores");
|
||||
}
|
||||
|
||||
info!(files=?ignores.iter().map(|ig| ig.path.as_path()).collect::<Vec<_>>(), "found some ignores");
|
||||
Ok(ignores)
|
||||
}
|
71
crates/cli/src/emits.rs
Normal file
71
crates/cli/src/emits.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use std::{ffi::OsString, fmt::Write, path::PathBuf};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use watchexec::paths::summarise_events_to_env;
|
||||
use watchexec_events::{filekind::FileEventKind, Event, Tag};
|
||||
|
||||
use crate::state::RotatingTempFile;
|
||||
|
||||
pub fn emits_to_environment(events: &[Event]) -> impl Iterator<Item = (String, OsString)> {
|
||||
summarise_events_to_env(events.iter())
|
||||
.into_iter()
|
||||
.map(|(k, v)| (format!("WATCHEXEC_{k}_PATH"), v))
|
||||
}
|
||||
|
||||
pub fn events_to_simple_format(events: &[Event]) -> Result<String> {
|
||||
let mut buf = String::new();
|
||||
for event in events {
|
||||
let feks = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| match tag {
|
||||
Tag::FileEventKind(kind) => Some(kind),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for path in event.paths().map(|(p, _)| p) {
|
||||
if feks.is_empty() {
|
||||
writeln!(&mut buf, "other:{}", path.to_string_lossy()).into_diagnostic()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
for fek in &feks {
|
||||
writeln!(
|
||||
&mut buf,
|
||||
"{}:{}",
|
||||
match fek {
|
||||
FileEventKind::Any | FileEventKind::Other => "other",
|
||||
FileEventKind::Access(_) => "access",
|
||||
FileEventKind::Create(_) => "create",
|
||||
FileEventKind::Modify(_) => "modify",
|
||||
FileEventKind::Remove(_) => "remove",
|
||||
},
|
||||
path.to_string_lossy()
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub fn emits_to_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
|
||||
target.rotate()?;
|
||||
target.write(events_to_simple_format(events)?.as_bytes())?;
|
||||
Ok(target.path())
|
||||
}
|
||||
|
||||
pub fn emits_to_json_file(target: &RotatingTempFile, events: &[Event]) -> Result<PathBuf> {
|
||||
target.rotate()?;
|
||||
for event in events {
|
||||
if event.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
target.write(&serde_json::to_vec(event).into_diagnostic()?)?;
|
||||
target.write(b"\n")?;
|
||||
}
|
||||
Ok(target.path())
|
||||
}
|
188
crates/cli/src/filterer.rs
Normal file
188
crates/cli/src/filterer.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tracing::{info, trace, trace_span};
|
||||
use watchexec::{error::RuntimeError, filter::Filterer};
|
||||
use watchexec_events::{
|
||||
filekind::{FileEventKind, ModifyKind},
|
||||
Event, Priority, Tag,
|
||||
};
|
||||
use watchexec_filterer_globset::GlobsetFilterer;
|
||||
|
||||
use crate::args::{Args, FsEvent};
|
||||
|
||||
pub(crate) mod parse;
|
||||
mod proglib;
|
||||
mod progs;
|
||||
mod syncval;
|
||||
|
||||
/// A custom filterer that combines the library's Globset filterer and a switch for --no-meta
|
||||
#[derive(Debug)]
|
||||
pub struct WatchexecFilterer {
|
||||
inner: GlobsetFilterer,
|
||||
fs_events: Vec<FsEvent>,
|
||||
progs: Option<progs::FilterProgs>,
|
||||
}
|
||||
|
||||
impl Filterer for WatchexecFilterer {
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
fn check_event(&self, event: &Event, priority: Priority) -> Result<bool, RuntimeError> {
|
||||
for tag in &event.tags {
|
||||
if let Tag::FileEventKind(fek) = tag {
|
||||
let normalised = match fek {
|
||||
FileEventKind::Access(_) => FsEvent::Access,
|
||||
FileEventKind::Modify(ModifyKind::Name(_)) => FsEvent::Rename,
|
||||
FileEventKind::Modify(ModifyKind::Metadata(_)) => FsEvent::Metadata,
|
||||
FileEventKind::Modify(_) => FsEvent::Modify,
|
||||
FileEventKind::Create(_) => FsEvent::Create,
|
||||
FileEventKind::Remove(_) => FsEvent::Remove,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
trace!(allowed=?self.fs_events, this=?normalised, "check against fs event filter");
|
||||
if !self.fs_events.contains(&normalised) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("check against original event");
|
||||
if !self.inner.check_event(event, priority)? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(progs) = &self.progs {
|
||||
trace!("check against program filters");
|
||||
if !progs.check(event)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchexecFilterer {
|
||||
/// Create a new filterer from the given arguments
|
||||
pub async fn new(args: &Args) -> Result<Arc<Self>> {
|
||||
let project_origin = args.project_origin.clone().unwrap();
|
||||
let workdir = args.workdir.clone().unwrap();
|
||||
|
||||
let ignore_files = if args.no_discover_ignore {
|
||||
Vec::new()
|
||||
} else {
|
||||
let vcs_types = crate::dirs::vcs_types(&project_origin).await;
|
||||
crate::dirs::ignores(args, &vcs_types).await?
|
||||
};
|
||||
|
||||
let mut ignores = Vec::new();
|
||||
|
||||
if !args.no_default_ignore {
|
||||
ignores.extend([
|
||||
(format!("**{MAIN_SEPARATOR}.DS_Store"), None),
|
||||
(String::from("watchexec.*.log"), None),
|
||||
(String::from("*.py[co]"), None),
|
||||
(String::from("#*#"), None),
|
||||
(String::from(".#*"), None),
|
||||
(String::from(".*.kate-swp"), None),
|
||||
(String::from(".*.sw?"), None),
|
||||
(String::from(".*.sw?x"), None),
|
||||
(format!("**{MAIN_SEPARATOR}.bzr{MAIN_SEPARATOR}**"), None),
|
||||
(format!("**{MAIN_SEPARATOR}_darcs{MAIN_SEPARATOR}**"), None),
|
||||
(
|
||||
format!("**{MAIN_SEPARATOR}.fossil-settings{MAIN_SEPARATOR}**"),
|
||||
None,
|
||||
),
|
||||
(format!("**{MAIN_SEPARATOR}.git{MAIN_SEPARATOR}**"), None),
|
||||
(format!("**{MAIN_SEPARATOR}.hg{MAIN_SEPARATOR}**"), None),
|
||||
(format!("**{MAIN_SEPARATOR}.pijul{MAIN_SEPARATOR}**"), None),
|
||||
(format!("**{MAIN_SEPARATOR}.svn{MAIN_SEPARATOR}**"), None),
|
||||
]);
|
||||
}
|
||||
|
||||
let whitelist = args
|
||||
.paths
|
||||
.iter()
|
||||
.map(|p| p.into())
|
||||
.filter(|p: &PathBuf| p.is_file());
|
||||
|
||||
let mut filters = args
|
||||
.filter_patterns
|
||||
.iter()
|
||||
.map(|f| (f.to_owned(), Some(workdir.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for filter_file in &args.filter_files {
|
||||
filters.extend(read_filter_file(filter_file).await?);
|
||||
}
|
||||
|
||||
ignores.extend(
|
||||
args.ignore_patterns
|
||||
.iter()
|
||||
.map(|f| (f.to_owned(), Some(workdir.clone()))),
|
||||
);
|
||||
|
||||
let exts = args
|
||||
.filter_extensions
|
||||
.iter()
|
||||
.map(|e| OsString::from(e.strip_prefix('.').unwrap_or(e)));
|
||||
|
||||
info!("initialising Globset filterer");
|
||||
Ok(Arc::new(Self {
|
||||
inner: GlobsetFilterer::new(
|
||||
project_origin,
|
||||
filters,
|
||||
ignores,
|
||||
whitelist,
|
||||
ignore_files,
|
||||
exts,
|
||||
)
|
||||
.await
|
||||
.into_diagnostic()?,
|
||||
fs_events: args.filter_fs_events.clone(),
|
||||
progs: if args.filter_programs_parsed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(progs::FilterProgs::new(args)?)
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_filter_file(path: &Path) -> Result<Vec<(String, Option<PathBuf>)>> {
|
||||
let _span = trace_span!("loading filter file", ?path).entered();
|
||||
|
||||
let file = tokio::fs::File::open(path).await.into_diagnostic()?;
|
||||
|
||||
let metadata_len = file
|
||||
.metadata()
|
||||
.await
|
||||
.map(|m| usize::try_from(m.len()))
|
||||
.unwrap_or(Ok(0))
|
||||
.into_diagnostic()?;
|
||||
let filter_capacity = if metadata_len == 0 {
|
||||
0
|
||||
} else {
|
||||
metadata_len / 20
|
||||
};
|
||||
let mut filters = Vec::with_capacity(filter_capacity);
|
||||
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
while let Some(line) = lines.next_line().await.into_diagnostic()? {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
trace!(?line, "adding filter line");
|
||||
filters.push((line.to_owned(), Some(path.to_owned())));
|
||||
}
|
||||
|
||||
Ok(filters)
|
||||
}
|
17
crates/cli/src/filterer/parse.rs
Normal file
17
crates/cli/src/filterer/parse.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
use miette::{miette, Result};
|
||||
|
||||
pub fn parse_filter_program((n, prog): (usize, String)) -> Result<jaq_syn::Main> {
|
||||
let parser = jaq_parse::main();
|
||||
let (main, errs) = jaq_parse::parse(&prog, parser);
|
||||
|
||||
if !errs.is_empty() {
|
||||
let errs = errs
|
||||
.into_iter()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
return Err(miette!("{}", errs).wrap_err(format!("failed to load filter program #{}", n)));
|
||||
}
|
||||
|
||||
main.ok_or_else(|| miette!("failed to load filter program #{} (no reason given)", n))
|
||||
}
|
27
crates/cli/src/filterer/proglib.rs
Normal file
27
crates/cli/src/filterer/proglib.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use jaq_interpret::ParseCtx;
|
||||
use miette::Result;
|
||||
use tracing::debug;
|
||||
|
||||
mod file;
|
||||
mod hash;
|
||||
mod kv;
|
||||
mod macros;
|
||||
mod output;
|
||||
|
||||
pub fn jaq_lib() -> Result<ParseCtx> {
|
||||
let mut jaq = ParseCtx::new(Vec::new());
|
||||
|
||||
debug!("loading jaq core library");
|
||||
jaq.insert_natives(jaq_core::core());
|
||||
|
||||
debug!("loading jaq std library");
|
||||
jaq.insert_defs(jaq_std::std());
|
||||
|
||||
debug!("loading jaq watchexec library");
|
||||
file::load(&mut jaq);
|
||||
hash::load(&mut jaq);
|
||||
kv::load(&mut jaq);
|
||||
output::load(&mut jaq);
|
||||
|
||||
Ok(jaq)
|
||||
}
|
173
crates/cli/src/filterer/proglib/file.rs
Normal file
173
crates/cli/src/filterer/proglib/file.rs
Normal file
|
@ -0,0 +1,173 @@
|
|||
use std::{
|
||||
fs::{metadata, File, FileType, Metadata},
|
||||
io::{BufReader, Read},
|
||||
iter::once,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use jaq_interpret::{Error, Native, ParseCtx, Val};
|
||||
use serde_json::{json, Value};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::macros::*;
|
||||
|
||||
pub fn load(jaq: &mut ParseCtx) {
|
||||
trace!("jaq: add file_read filter");
|
||||
jaq.insert_native(
|
||||
"file_read".into(),
|
||||
1,
|
||||
Native::new({
|
||||
move |args, (ctx, val)| {
|
||||
let path = match &val {
|
||||
Val::Str(v) => v.to_string(),
|
||||
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
|
||||
};
|
||||
|
||||
let bytes = match int_arg!(args, 0, ctx, &val) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!(Err(e)),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(match File::open(&path) {
|
||||
Ok(file) => {
|
||||
let buf_reader = BufReader::new(file);
|
||||
let mut limited = buf_reader.take(bytes);
|
||||
let mut buffer = String::with_capacity(bytes as _);
|
||||
match limited.read_to_string(&mut buffer) {
|
||||
Ok(read) => {
|
||||
debug!("jaq: read {read} bytes from {path:?}");
|
||||
Val::Str(buffer.into())
|
||||
}
|
||||
Err(err) => {
|
||||
error!("jaq: failed to read from {path:?}: {err:?}");
|
||||
Val::Null
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("jaq: failed to open file {path:?}: {err:?}");
|
||||
Val::Null
|
||||
}
|
||||
})))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
trace!("jaq: add file_meta filter");
|
||||
jaq.insert_native(
|
||||
"file_meta".into(),
|
||||
0,
|
||||
Native::new({
|
||||
move |_, (_, val)| {
|
||||
let path = match &val {
|
||||
Val::Str(v) => v.to_string(),
|
||||
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(match metadata(&path) {
|
||||
Ok(meta) => Val::from(json_meta(meta)),
|
||||
Err(err) => {
|
||||
error!("jaq: failed to open {path:?}: {err:?}");
|
||||
Val::Null
|
||||
}
|
||||
})))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
trace!("jaq: add file_size filter");
|
||||
jaq.insert_native(
|
||||
"file_size".into(),
|
||||
0,
|
||||
Native::new({
|
||||
move |_, (_, val)| {
|
||||
let path = match &val {
|
||||
Val::Str(v) => v.to_string(),
|
||||
_ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(match metadata(&path) {
|
||||
Ok(meta) => Val::Int(meta.len() as _),
|
||||
Err(err) => {
|
||||
error!("jaq: failed to open {path:?}: {err:?}");
|
||||
Val::Null
|
||||
}
|
||||
})))
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn json_meta(meta: Metadata) -> Value {
|
||||
let perms = meta.permissions();
|
||||
let mut val = json!({
|
||||
"type": filetype_str(meta.file_type()),
|
||||
"size": meta.len(),
|
||||
"modified": fs_time(meta.modified()),
|
||||
"accessed": fs_time(meta.accessed()),
|
||||
"created": fs_time(meta.created()),
|
||||
"dir": meta.is_dir(),
|
||||
"file": meta.is_file(),
|
||||
"symlink": meta.is_symlink(),
|
||||
"readonly": perms.readonly(),
|
||||
});
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let map = val.as_object_mut().unwrap();
|
||||
map.insert(
|
||||
"mode".to_string(),
|
||||
Value::String(format!("{:o}", perms.mode())),
|
||||
);
|
||||
map.insert("mode_byte".to_string(), Value::from(perms.mode()));
|
||||
map.insert(
|
||||
"executable".to_string(),
|
||||
Value::Bool(perms.mode() & 0o111 != 0),
|
||||
);
|
||||
}
|
||||
|
||||
val
|
||||
}
|
||||
|
||||
fn filetype_str(filetype: FileType) -> &'static str {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::FileTypeExt;
|
||||
if filetype.is_char_device() {
|
||||
return "char";
|
||||
} else if filetype.is_block_device() {
|
||||
return "block";
|
||||
} else if filetype.is_fifo() {
|
||||
return "fifo";
|
||||
} else if filetype.is_socket() {
|
||||
return "socket";
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::fs::FileTypeExt;
|
||||
if filetype.is_symlink_dir() {
|
||||
return "symdir";
|
||||
} else if filetype.is_symlink_file() {
|
||||
return "symfile";
|
||||
}
|
||||
}
|
||||
|
||||
if filetype.is_dir() {
|
||||
"dir"
|
||||
} else if filetype.is_file() {
|
||||
"file"
|
||||
} else if filetype.is_symlink() {
|
||||
"symlink"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn fs_time(time: std::io::Result<SystemTime>) -> Option<u64> {
|
||||
time.ok()
|
||||
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
|
||||
.map(|dur| dur.as_secs())
|
||||
}
|
62
crates/cli/src/filterer/proglib/hash.rs
Normal file
62
crates/cli/src/filterer/proglib/hash.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use std::{fs::File, io::Read, iter::once};
|
||||
|
||||
use jaq_interpret::{Error, Native, ParseCtx, Val};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::macros::*;
|
||||
|
||||
pub fn load(jaq: &mut ParseCtx) {
|
||||
trace!("jaq: add hash filter");
|
||||
jaq.insert_native(
|
||||
"hash".into(),
|
||||
0,
|
||||
Native::new({
|
||||
move |_, (_, val)| {
|
||||
let string = match &val {
|
||||
Val::Str(v) => v.to_string(),
|
||||
_ => return_err!(Err(Error::str("expected string but got {val:?}"))),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(Val::Str(
|
||||
blake3::hash(string.as_bytes()).to_hex().to_string().into(),
|
||||
))))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
trace!("jaq: add file_hash filter");
|
||||
jaq.insert_native(
|
||||
"file_hash".into(),
|
||||
0,
|
||||
Native::new({
|
||||
move |_, (_, val)| {
|
||||
let path = match &val {
|
||||
Val::Str(v) => v.to_string(),
|
||||
_ => return_err!(Err(Error::str("expected string but got {val:?}"))),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(match File::open(&path) {
|
||||
Ok(mut file) => {
|
||||
const BUFFER_SIZE: usize = 1024 * 1024;
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
let mut buf = vec![0; BUFFER_SIZE];
|
||||
while let Ok(bytes) = file.read(&mut buf) {
|
||||
debug!("jaq: read {bytes} bytes from {path:?}");
|
||||
if bytes == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buf[..bytes]);
|
||||
buf = vec![0; BUFFER_SIZE];
|
||||
}
|
||||
|
||||
Val::Str(hasher.finalize().to_hex().to_string().into())
|
||||
}
|
||||
Err(err) => {
|
||||
error!("jaq: failed to open file {path:?}: {err:?}");
|
||||
Val::Null
|
||||
}
|
||||
})))
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
69
crates/cli/src/filterer/proglib/kv.rs
Normal file
69
crates/cli/src/filterer/proglib/kv.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::{iter::once, sync::Arc};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use jaq_interpret::{Error, Native, ParseCtx, Val};
|
||||
use once_cell::sync::OnceCell;
|
||||
use tracing::trace;
|
||||
|
||||
use crate::filterer::syncval::SyncVal;
|
||||
|
||||
use super::macros::*;
|
||||
|
||||
type KvStore = Arc<DashMap<String, SyncVal>>;
|
||||
fn kv_store() -> KvStore {
|
||||
static KV_STORE: OnceCell<KvStore> = OnceCell::new();
|
||||
KV_STORE.get_or_init(|| KvStore::default()).clone()
|
||||
}
|
||||
|
||||
pub fn load(jaq: &mut ParseCtx) {
|
||||
trace!("jaq: add kv_clear filter");
|
||||
jaq.insert_native(
|
||||
"kv_clear".into(),
|
||||
0,
|
||||
Native::new({
|
||||
move |_, (_, val)| {
|
||||
let kv = kv_store();
|
||||
kv.clear();
|
||||
Box::new(once(Ok(val)))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
trace!("jaq: add kv_store filter");
|
||||
jaq.insert_native(
|
||||
"kv_store".into(),
|
||||
1,
|
||||
Native::new({
|
||||
move |args, (ctx, val)| {
|
||||
let kv = kv_store();
|
||||
let key = match string_arg!(args, 0, ctx, val) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!(Err(e)),
|
||||
};
|
||||
|
||||
kv.insert(key, (&val).into());
|
||||
Box::new(once(Ok(val)))
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
trace!("jaq: add kv_fetch filter");
|
||||
jaq.insert_native(
|
||||
"kv_fetch".into(),
|
||||
1,
|
||||
Native::new({
|
||||
move |args, (ctx, val)| {
|
||||
let kv = kv_store();
|
||||
let key = match string_arg!(args, 0, ctx, val) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!(Err(e)),
|
||||
};
|
||||
|
||||
Box::new(once(Ok(kv
|
||||
.get(&key)
|
||||
.map(|val| val.value().into())
|
||||
.unwrap_or(Val::Null))))
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
30
crates/cli/src/filterer/proglib/macros.rs
Normal file
30
crates/cli/src/filterer/proglib/macros.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
macro_rules! return_err {
|
||||
($err:expr) => {
|
||||
return Box::new(once($err))
|
||||
};
|
||||
}
|
||||
pub(crate) use return_err;
|
||||
|
||||
macro_rules! string_arg {
|
||||
($args:expr, $n:expr, $ctx:expr, $val:expr) => {
|
||||
match ::jaq_interpret::FilterT::run($args.get($n), ($ctx.clone(), $val.clone())).next() {
|
||||
Some(Ok(Val::Str(v))) => Ok(v.to_string()),
|
||||
Some(Ok(val)) => Err(Error::str(format!("expected string but got {val:?}"))),
|
||||
Some(Err(e)) => Err(e),
|
||||
None => Err(Error::str("value expected but none found")),
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use string_arg;
|
||||
|
||||
macro_rules! int_arg {
|
||||
($args:expr, $n:expr, $ctx:expr, $val:expr) => {
|
||||
match ::jaq_interpret::FilterT::run($args.get($n), ($ctx.clone(), $val.clone())).next() {
|
||||
Some(Ok(Val::Int(v))) => Ok(v as _),
|
||||
Some(Ok(val)) => Err(Error::str(format!("expected int but got {val:?}"))),
|
||||
Some(Err(e)) => Err(e),
|
||||
None => Err(Error::str("value expected but none found")),
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use int_arg;
|
83
crates/cli/src/filterer/proglib/output.rs
Normal file
83
crates/cli/src/filterer/proglib/output.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use std::iter::once;
|
||||
|
||||
use jaq_interpret::{Error, Native, ParseCtx, Val};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::macros::*;
|
||||
|
||||
macro_rules! log_action {
|
||||
($level:expr, $val:expr) => {
|
||||
match $level.to_ascii_lowercase().as_str() {
|
||||
"trace" => trace!("jaq: {}", $val),
|
||||
"debug" => debug!("jaq: {}", $val),
|
||||
"info" => info!("jaq: {}", $val),
|
||||
"warn" => warn!("jaq: {}", $val),
|
||||
"error" => error!("jaq: {}", $val),
|
||||
_ => return_err!(Err(Error::str("invalid log level"))),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn load(jaq: &mut ParseCtx) {
|
||||
trace!("jaq: add log filter");
|
||||
jaq.insert_native(
|
||||
"log".into(),
|
||||
1,
|
||||
Native::with_update(
|
||||
|args, (ctx, val)| {
|
||||
let level = match string_arg!(args, 0, ctx, val) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!(Err(e)),
|
||||
};
|
||||
|
||||
log_action!(level, val);
|
||||
|
||||
// passthrough
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
|args, (ctx, val), _| {
|
||||
let level = match string_arg!(args, 0, ctx, val) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return_err!(Err(e)),
|
||||
};
|
||||
|
||||
log_action!(level, val);
|
||||
|
||||
// passthrough
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
trace!("jaq: add printout filter");
|
||||
jaq.insert_native(
|
||||
"printout".into(),
|
||||
0,
|
||||
Native::with_update(
|
||||
|_, (_, val)| {
|
||||
println!("{}", val);
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
|_, (_, val), _| {
|
||||
println!("{}", val);
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
trace!("jaq: add printerr filter");
|
||||
jaq.insert_native(
|
||||
"printerr".into(),
|
||||
0,
|
||||
Native::with_update(
|
||||
|_, (_, val)| {
|
||||
eprintln!("{}", val);
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
|_, (_, val), _| {
|
||||
eprintln!("{}", val);
|
||||
Box::new(once(Ok(val)))
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
143
crates/cli/src/filterer/progs.rs
Normal file
143
crates/cli/src/filterer/progs.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use std::{iter::empty, marker::PhantomData};
|
||||
|
||||
use jaq_interpret::{Ctx, FilterT, RcIter, Val};
|
||||
use miette::miette;
|
||||
use tokio::{
|
||||
sync::{mpsc, oneshot},
|
||||
task::{block_in_place, spawn_blocking},
|
||||
};
|
||||
use tracing::{error, trace, warn};
|
||||
use watchexec::error::RuntimeError;
|
||||
use watchexec_events::Event;
|
||||
|
||||
use crate::args::Args;
|
||||
|
||||
const BUFFER: usize = 128;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FilterProgs {
|
||||
channel: Requester<Event, bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Requester<S, R> {
|
||||
sender: mpsc::Sender<(S, oneshot::Sender<R>)>,
|
||||
_receiver: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<S, R> Requester<S, R>
|
||||
where
|
||||
S: Send + Sync,
|
||||
R: Send + Sync,
|
||||
{
|
||||
pub fn new(capacity: usize) -> (Self, mpsc::Receiver<(S, oneshot::Sender<R>)>) {
|
||||
let (sender, receiver) = mpsc::channel(capacity);
|
||||
(
|
||||
Self {
|
||||
sender,
|
||||
_receiver: PhantomData,
|
||||
},
|
||||
receiver,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn call(&self, value: S) -> Result<R, RuntimeError> {
|
||||
// FIXME: this should really be async with a timeout, but that needs filtering in general
|
||||
// to be async, which should be done at some point
|
||||
block_in_place(|| {
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
self.sender.blocking_send((value, sender)).map_err(|err| {
|
||||
RuntimeError::External(miette!("filter progs internal channel: {}", err).into())
|
||||
})?;
|
||||
receiver
|
||||
.blocking_recv()
|
||||
.map_err(|err| RuntimeError::External(Box::new(err)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FilterProgs {
|
||||
pub fn check(&self, event: &Event) -> Result<bool, RuntimeError> {
|
||||
self.channel.call(event.clone())
|
||||
}
|
||||
|
||||
pub fn new(args: &Args) -> miette::Result<Self> {
|
||||
let progs = args.filter_programs_parsed.clone();
|
||||
eprintln!(
|
||||
"EXPERIMENTAL: filter programs are unstable and may change/vanish without notice"
|
||||
);
|
||||
|
||||
let (requester, mut receiver) = Requester::<Event, bool>::new(BUFFER);
|
||||
let task =
|
||||
spawn_blocking(move || {
|
||||
'chan: while let Some((event, sender)) = receiver.blocking_recv() {
|
||||
let val = serde_json::to_value(&event)
|
||||
.map_err(|err| miette!("failed to serialize event: {}", err))
|
||||
.map(Val::from)?;
|
||||
|
||||
for (n, prog) in progs.iter().enumerate() {
|
||||
trace!(?n, "trying filter program");
|
||||
let mut jaq = super::proglib::jaq_lib()?;
|
||||
let filter = jaq.compile(prog.clone());
|
||||
if !jaq.errs.is_empty() {
|
||||
for (error, span) in jaq.errs {
|
||||
error!(%error, "failed to compile filter program #{n}@{}:{}", span.start, span.end);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let inputs = RcIter::new(empty());
|
||||
let mut results = filter.run((Ctx::new([], &inputs), val.clone()));
|
||||
if let Some(res) = results.next() {
|
||||
match res {
|
||||
Ok(Val::Bool(false)) => {
|
||||
trace!(
|
||||
?n,
|
||||
verdict = false,
|
||||
"filter program finished; fail so stopping there"
|
||||
);
|
||||
sender
|
||||
.send(false)
|
||||
.unwrap_or_else(|_| warn!("failed to send filter result"));
|
||||
continue 'chan;
|
||||
}
|
||||
Ok(Val::Bool(true)) => {
|
||||
trace!(
|
||||
?n,
|
||||
verdict = true,
|
||||
"filter program finished; pass so trying next"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(val) => {
|
||||
error!(?n, ?val, "filter program returned non-boolean, ignoring and trying next");
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?n, error=%err, "filter program failed, so trying next");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("all filters failed, sending pass as default");
|
||||
sender
|
||||
.send(true)
|
||||
.unwrap_or_else(|_| warn!("failed to send filter result"));
|
||||
}
|
||||
|
||||
Ok(()) as miette::Result<()>
|
||||
});
|
||||
|
||||
tokio::spawn(async {
|
||||
match task.await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => error!("filter progs task failed: {}", err),
|
||||
Err(err) => error!("filter progs task panicked: {}", err),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self { channel: requester })
|
||||
}
|
||||
}
|
71
crates/cli/src/filterer/syncval.rs
Normal file
71
crates/cli/src/filterer/syncval.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
/// Jaq's [Val](jaq_interpret::Val) uses Rc, but we want to use in Sync contexts. UGH!
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use jaq_interpret::Val;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum SyncVal {
|
||||
Null,
|
||||
Bool(bool),
|
||||
Int(isize),
|
||||
Float(f64),
|
||||
Num(Arc<str>),
|
||||
Str(Arc<str>),
|
||||
Arr(Arc<[SyncVal]>),
|
||||
Obj(Arc<IndexMap<Arc<str>, SyncVal>>),
|
||||
}
|
||||
|
||||
impl From<&Val> for SyncVal {
|
||||
fn from(val: &Val) -> Self {
|
||||
match val {
|
||||
Val::Null => Self::Null,
|
||||
Val::Bool(b) => Self::Bool(*b),
|
||||
Val::Int(i) => Self::Int(*i),
|
||||
Val::Float(f) => Self::Float(*f),
|
||||
Val::Num(s) => Self::Num(s.to_string().into()),
|
||||
Val::Str(s) => Self::Str(s.to_string().into()),
|
||||
Val::Arr(a) => Self::Arr({
|
||||
let mut arr = Vec::with_capacity(a.len());
|
||||
for v in a.iter() {
|
||||
arr.push(v.into());
|
||||
}
|
||||
arr.into()
|
||||
}),
|
||||
Val::Obj(m) => Self::Obj(Arc::new({
|
||||
let mut map = IndexMap::new();
|
||||
for (k, v) in m.iter() {
|
||||
map.insert(k.to_string().into(), v.into());
|
||||
}
|
||||
map
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SyncVal> for Val {
|
||||
fn from(val: &SyncVal) -> Self {
|
||||
match val {
|
||||
SyncVal::Null => Self::Null,
|
||||
SyncVal::Bool(b) => Self::Bool(*b),
|
||||
SyncVal::Int(i) => Self::Int(*i),
|
||||
SyncVal::Float(f) => Self::Float(*f),
|
||||
SyncVal::Num(s) => Self::Num(s.to_string().into()),
|
||||
SyncVal::Str(s) => Self::Str(s.to_string().into()),
|
||||
SyncVal::Arr(a) => Self::Arr({
|
||||
let mut arr = Vec::with_capacity(a.len());
|
||||
for v in a.iter() {
|
||||
arr.push(v.into());
|
||||
}
|
||||
arr.into()
|
||||
}),
|
||||
SyncVal::Obj(m) => Self::Obj(Rc::new({
|
||||
let mut map: IndexMap<_, _, ahash::RandomState> = Default::default();
|
||||
for (k, v) in m.iter() {
|
||||
map.insert(k.to_string().into(), v.into());
|
||||
}
|
||||
map
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
128
crates/cli/src/lib.rs
Normal file
128
crates/cli/src/lib.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::missing_const_for_fn, clippy::future_not_send)]
|
||||
|
||||
use std::{io::Write, process::Stdio};
|
||||
|
||||
use args::{Args, ShellCompletion};
|
||||
use clap::CommandFactory;
|
||||
use clap_complete::{Generator, Shell};
|
||||
use clap_mangen::Man;
|
||||
use is_terminal::IsTerminal;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tokio::{io::AsyncWriteExt, process::Command};
|
||||
use tracing::{debug, info};
|
||||
use watchexec::Watchexec;
|
||||
use watchexec_events::{Event, Priority};
|
||||
|
||||
use crate::filterer::WatchexecFilterer;
|
||||
|
||||
pub mod args;
|
||||
mod config;
|
||||
mod dirs;
|
||||
mod emits;
|
||||
mod filterer;
|
||||
mod state;
|
||||
|
||||
async fn run_watchexec(args: Args) -> Result<()> {
|
||||
info!(version=%env!("CARGO_PKG_VERSION"), "constructing Watchexec from CLI");
|
||||
|
||||
let state = state::State::default();
|
||||
let config = config::make_config(&args, &state)?;
|
||||
config.filterer(WatchexecFilterer::new(&args).await?);
|
||||
|
||||
info!("initialising Watchexec runtime");
|
||||
let wx = Watchexec::with_config(config)?;
|
||||
|
||||
if !args.postpone {
|
||||
debug!("kicking off with empty event");
|
||||
wx.send_event(Event::default(), Priority::Urgent).await?;
|
||||
}
|
||||
|
||||
info!("running main loop");
|
||||
wx.main().await.into_diagnostic()??;
|
||||
|
||||
if matches!(args.screen_clear, Some(args::ClearMode::Reset)) {
|
||||
config::reset_screen();
|
||||
}
|
||||
|
||||
info!("done with main loop");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_manpage(_args: Args) -> Result<()> {
|
||||
info!(version=%env!("CARGO_PKG_VERSION"), "constructing manpage");
|
||||
|
||||
let man = Man::new(Args::command().long_version(None));
|
||||
let mut buffer: Vec<u8> = Default::default();
|
||||
man.render(&mut buffer).into_diagnostic()?;
|
||||
|
||||
if std::io::stdout().is_terminal() && which::which("man").is_ok() {
|
||||
let mut child = Command::new("man")
|
||||
.arg("-l")
|
||||
.arg("-")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.into_diagnostic()?;
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&buffer)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
if let Some(code) = child
|
||||
.wait()
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.code()
|
||||
.and_then(|code| if code == 0 { None } else { Some(code) })
|
||||
{
|
||||
return Err(miette::miette!("Exited with status code {}", code));
|
||||
}
|
||||
} else {
|
||||
std::io::stdout()
|
||||
.lock()
|
||||
.write_all(&buffer)
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
async fn run_completions(shell: ShellCompletion) -> Result<()> {
|
||||
fn generate(generator: impl Generator) {
|
||||
let mut cmd = Args::command();
|
||||
clap_complete::generate(generator, &mut cmd, "watchexec", &mut std::io::stdout());
|
||||
}
|
||||
|
||||
info!(version=%env!("CARGO_PKG_VERSION"), "constructing completions");
|
||||
|
||||
match shell {
|
||||
ShellCompletion::Bash => generate(Shell::Bash),
|
||||
ShellCompletion::Elvish => generate(Shell::Elvish),
|
||||
ShellCompletion::Fish => generate(Shell::Fish),
|
||||
ShellCompletion::Nu => generate(clap_complete_nushell::Nushell),
|
||||
ShellCompletion::Powershell => generate(Shell::PowerShell),
|
||||
ShellCompletion::Zsh => generate(Shell::Zsh),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
let (args, _log_guard) = args::get_args().await?;
|
||||
|
||||
if args.manual {
|
||||
run_manpage(args).await
|
||||
} else if let Some(shell) = args.completions {
|
||||
run_completions(shell).await
|
||||
} else {
|
||||
run_watchexec(args).await
|
||||
}
|
||||
}
|
22
crates/cli/src/main.rs
Normal file
22
crates/cli/src/main.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
#[cfg(feature = "eyra")]
|
||||
extern crate eyra;
|
||||
|
||||
use miette::IntoDiagnostic;
|
||||
|
||||
#[cfg(target_env = "musl")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
fn main() -> miette::Result<()> {
|
||||
#[cfg(feature = "pid1")]
|
||||
pid1::Pid1Settings::new()
|
||||
.enable_log(cfg!(feature = "pid1-withlog"))
|
||||
.launch()
|
||||
.into_diagnostic()?;
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async { watchexec_cli::run().await })
|
||||
}
|
48
crates/cli/src/state.rs
Normal file
48
crates/cli/src/state.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use std::{
|
||||
env::var_os,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct State {
|
||||
pub emit_file: RotatingTempFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RotatingTempFile(Arc<Mutex<Option<NamedTempFile>>>);
|
||||
|
||||
impl RotatingTempFile {
|
||||
pub fn rotate(&self) -> Result<()> {
|
||||
// implicitly drops the old file
|
||||
*self.0.lock().unwrap() = Some(
|
||||
if let Some(dir) = var_os("WATCHEXEC_TMPDIR") {
|
||||
NamedTempFile::new_in(dir)
|
||||
} else {
|
||||
NamedTempFile::new()
|
||||
}
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(&self, data: &[u8]) -> Result<()> {
|
||||
if let Some(file) = self.0.lock().unwrap().as_mut() {
|
||||
file.write_all(data).into_diagnostic()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
if let Some(file) = self.0.lock().unwrap().as_ref() {
|
||||
file.path().to_owned()
|
||||
} else {
|
||||
PathBuf::new()
|
||||
}
|
||||
}
|
||||
}
|
121
crates/cli/tests/common/mod.rs
Normal file
121
crates/cli/tests/common/mod.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use std::path::PathBuf;
|
||||
use std::{fs, sync::OnceLock};
|
||||
|
||||
use miette::{Context, IntoDiagnostic, Result};
|
||||
use rand::Rng;
|
||||
|
||||
static PLACEHOLDER_DATA: OnceLock<String> = OnceLock::new();
|
||||
fn get_placeholder_data() -> &'static str {
|
||||
PLACEHOLDER_DATA.get_or_init(|| "PLACEHOLDER\n".repeat(500))
|
||||
}
|
||||
|
||||
/// The amount of nesting that will be used for generated files
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum GeneratedFileNesting {
|
||||
/// Only one level of files
|
||||
Flat,
|
||||
/// Random, up to a certiain maximum
|
||||
RandomToMax(usize),
|
||||
}
|
||||
|
||||
/// Configuration for creating testing subfolders
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TestSubfolderConfiguration {
|
||||
/// The amount of nesting that will be used when folders are generated
|
||||
pub(crate) nesting: GeneratedFileNesting,
|
||||
|
||||
/// Number of files the folder should contain
|
||||
pub(crate) file_count: usize,
|
||||
|
||||
/// Subfolder name
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
/// Options for generating test files
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub(crate) struct GenerateTestFilesArgs {
|
||||
/// The path where the files should be generated
|
||||
/// if None, the current working directory will be used.
|
||||
pub(crate) path: Option<PathBuf>,
|
||||
|
||||
/// Configurations for subfolders to generate
|
||||
pub(crate) subfolder_configs: Vec<TestSubfolderConfiguration>,
|
||||
}
|
||||
|
||||
/// Generate test files
|
||||
///
|
||||
/// This returns the same number of paths that were requested via subfolder_configs.
|
||||
pub(crate) fn generate_test_files(args: GenerateTestFilesArgs) -> Result<Vec<PathBuf>> {
|
||||
// Use or create a temporary directory for the test files
|
||||
let tmpdir = if let Some(p) = args.path {
|
||||
p
|
||||
} else {
|
||||
tempfile::tempdir()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to build tempdir")?
|
||||
.into_path()
|
||||
};
|
||||
let mut paths = vec![tmpdir.clone()];
|
||||
|
||||
// Generate subfolders matching each config
|
||||
for subfolder_config in args.subfolder_configs.iter() {
|
||||
// Create the subfolder path
|
||||
let subfolder_path = tmpdir.join(&subfolder_config.name);
|
||||
fs::create_dir(&subfolder_path)
|
||||
.into_diagnostic()
|
||||
.wrap_err(format!(
|
||||
"failed to create path for dir [{}]",
|
||||
subfolder_path.display()
|
||||
))?;
|
||||
paths.push(subfolder_path.clone());
|
||||
|
||||
// Fill the subfolder with files
|
||||
match subfolder_config.nesting {
|
||||
GeneratedFileNesting::Flat => {
|
||||
for idx in 0..subfolder_config.file_count {
|
||||
// Write stub file contents
|
||||
fs::write(
|
||||
subfolder_path.join(format!("stub-file-{idx}")),
|
||||
get_placeholder_data(),
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err(format!(
|
||||
"failed to write temporary file in subfolder {} @ idx {idx}",
|
||||
subfolder_path.display()
|
||||
))?;
|
||||
}
|
||||
}
|
||||
GeneratedFileNesting::RandomToMax(max_depth) => {
|
||||
let mut generator = rand::thread_rng();
|
||||
for idx in 0..subfolder_config.file_count {
|
||||
// Build a randomized path up to max depth
|
||||
let mut generated_path = subfolder_path.clone();
|
||||
let depth = generator.gen_range(0..max_depth);
|
||||
for _ in 0..depth {
|
||||
generated_path.push("stub-dir");
|
||||
}
|
||||
// Create the path
|
||||
fs::create_dir_all(&generated_path)
|
||||
.into_diagnostic()
|
||||
.wrap_err(format!(
|
||||
"failed to create randomly generated path [{}]",
|
||||
generated_path.display()
|
||||
))?;
|
||||
|
||||
// Write stub file contents @ the new randomized path
|
||||
fs::write(
|
||||
generated_path.join(format!("stub-file-{idx}")),
|
||||
get_placeholder_data(),
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err(format!(
|
||||
"failed to write temporary file in subfolder {} @ idx {idx}",
|
||||
subfolder_path.display()
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
146
crates/cli/tests/ignore.rs
Normal file
146
crates/cli/tests/ignore.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use tokio::{process::Command, time::Instant};
|
||||
use tracing_test::traced_test;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod common;
|
||||
use common::{generate_test_files, GenerateTestFilesArgs};
|
||||
|
||||
use crate::common::{GeneratedFileNesting, TestSubfolderConfiguration};
|
||||
|
||||
/// Directory name that will be sued for the dir that *should* be watched
|
||||
const WATCH_DIR_NAME: &str = "watch";
|
||||
|
||||
/// The token that watch will echo every time a match is found
|
||||
const WATCH_TOKEN: &str = "updated";
|
||||
|
||||
/// Ensure that watchexec runtime does not increase with the
|
||||
/// number of *ignored* files in a given folder
|
||||
///
|
||||
/// This test creates two separate folders, one small and the other large
|
||||
///
|
||||
/// Each folder has two subfolders:
|
||||
/// - a shallow one to be watched, with a few files of single depth (20 files)
|
||||
/// - a deep one to be ignored, with many files at varying depths (small case 200 files, large case 200,000 files)
|
||||
///
|
||||
/// watchexec, when executed on *either* folder should *not* experience a more
|
||||
/// than 10x degradation in performance, because the vast majority of the files
|
||||
/// are supposed to be ignored to begin with.
|
||||
///
|
||||
/// When running the CLI on the root folders, it should *not* take a long time to start de
|
||||
#[tokio::test]
|
||||
#[traced_test]
|
||||
async fn e2e_ignore_many_files_200_000() -> Result<()> {
|
||||
// Create a tempfile so that drop will clean it up
|
||||
let small_test_dir = tempfile::tempdir()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to create tempdir for test use")?;
|
||||
|
||||
// Determine the watchexec bin to use & build arguments
|
||||
let wexec_bin = std::env::var("TEST_WATCHEXEC_BIN").unwrap_or(
|
||||
option_env!("CARGO_BIN_EXE_watchexec")
|
||||
.map(std::string::ToString::to_string)
|
||||
.unwrap_or("watchexec".into()),
|
||||
);
|
||||
let token = format!("{WATCH_TOKEN}-{}", Uuid::new_v4());
|
||||
let args: Vec<String> = vec![
|
||||
"-1".into(), // exit as soon as watch completes
|
||||
"--watch".into(),
|
||||
WATCH_DIR_NAME.into(),
|
||||
"echo".into(),
|
||||
token.clone(),
|
||||
];
|
||||
|
||||
// Generate a small directory of files containing dirs that *will* and will *not* be watched
|
||||
let [ref root_dir_path, _, _] = generate_test_files(GenerateTestFilesArgs {
|
||||
path: Some(PathBuf::from(small_test_dir.path())),
|
||||
subfolder_configs: vec![
|
||||
// Shallow folder will have a small number of files and won't be watched
|
||||
TestSubfolderConfiguration {
|
||||
name: "watch".into(),
|
||||
nesting: GeneratedFileNesting::Flat,
|
||||
file_count: 5,
|
||||
},
|
||||
// Deep folder will have *many* amll files and will be watched
|
||||
TestSubfolderConfiguration {
|
||||
name: "unrelated".into(),
|
||||
nesting: GeneratedFileNesting::RandomToMax(42),
|
||||
file_count: 200,
|
||||
},
|
||||
],
|
||||
})?[..] else {
|
||||
panic!("unexpected number of paths returned from generate_test_files");
|
||||
};
|
||||
|
||||
// Get the number of elapsed
|
||||
let small_elapsed = run_watchexec_cmd(&wexec_bin, root_dir_path, args.clone()).await?;
|
||||
|
||||
// Create a tempfile so that drop will clean it up
|
||||
let large_test_dir = tempfile::tempdir()
|
||||
.into_diagnostic()
|
||||
.wrap_err("failed to create tempdir for test use")?;
|
||||
|
||||
// Generate a *large* directory of files
|
||||
let [ref root_dir_path, _, _] = generate_test_files(GenerateTestFilesArgs {
|
||||
path: Some(PathBuf::from(large_test_dir.path())),
|
||||
subfolder_configs: vec![
|
||||
// Shallow folder will have a small number of files and won't be watched
|
||||
TestSubfolderConfiguration {
|
||||
name: "watch".into(),
|
||||
nesting: GeneratedFileNesting::Flat,
|
||||
file_count: 5,
|
||||
},
|
||||
// Deep folder will have *many* amll files and will be watched
|
||||
TestSubfolderConfiguration {
|
||||
name: "unrelated".into(),
|
||||
nesting: GeneratedFileNesting::RandomToMax(42),
|
||||
file_count: 200_000,
|
||||
},
|
||||
],
|
||||
})?[..] else {
|
||||
panic!("unexpected number of paths returned from generate_test_files");
|
||||
};
|
||||
|
||||
// Get the number of elapsed
|
||||
let large_elapsed = run_watchexec_cmd(&wexec_bin, root_dir_path, args.clone()).await?;
|
||||
|
||||
// We expect the ignores to not impact watchexec startup time at all
|
||||
// whether there are 200 files in there or 200k
|
||||
assert!(
|
||||
large_elapsed < small_elapsed * 10,
|
||||
"200k ignore folder ({:?}) took more than 10x more time ({:?}) than 200 ignore folder ({:?})",
|
||||
large_elapsed,
|
||||
small_elapsed * 10,
|
||||
small_elapsed,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run a watchexec command once
|
||||
async fn run_watchexec_cmd(
|
||||
wexec_bin: impl AsRef<str>,
|
||||
dir: impl AsRef<Path>,
|
||||
args: impl Into<Vec<String>>,
|
||||
) -> Result<Duration> {
|
||||
// Build the subprocess command
|
||||
let mut cmd = Command::new(wexec_bin.as_ref());
|
||||
cmd.args(args.into());
|
||||
cmd.current_dir(dir);
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let start = Instant::now();
|
||||
cmd.kill_on_drop(true)
|
||||
.output()
|
||||
.await
|
||||
.into_diagnostic()
|
||||
.wrap_err("fixed")?;
|
||||
|
||||
Ok(start.elapsed())
|
||||
}
|
2
crates/cli/watchexec-manifest.rc
Normal file
2
crates/cli/watchexec-manifest.rc
Normal file
|
@ -0,0 +1,2 @@
|
|||
#define RT_MANIFEST 24
|
||||
1 RT_MANIFEST "watchexec.exe.manifest"
|
41
crates/cli/watchexec.exe.manifest
Normal file
41
crates/cli/watchexec.exe.manifest
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Watchexec.Cli.watchexec"
|
||||
version="2.2.0.0"
|
||||
/>
|
||||
|
||||
<trustInfo>
|
||||
<security>
|
||||
<!--
|
||||
UAC settings:
|
||||
- app should run at same integrity level as calling process
|
||||
- app does not need to manipulate windows belonging to
|
||||
higher-integrity-level processes
|
||||
-->
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10, 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings xmlns:ws="http://schemas.microsoft.com/SMI/2020/WindowsSettings">
|
||||
<ws:longPathAware xmlns:ws="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</ws:longPathAware>
|
||||
<ws:activeCodePage xmlns:ws="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</ws:activeCodePage>
|
||||
<ws:heapType xmlns:ws="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</ws:heapType>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue