##// END OF EJS Templates
dirstate-tree: Add the new `status()` algorithm...
Simon Sapin -
r47883:be579775 default
parent child Browse files
Show More
@@ -1,1104 +1,1105 b''
1 1 # This file is automatically @generated by Cargo.
2 2 # It is not intended for manual editing.
3 3 [[package]]
4 4 name = "adler"
5 5 version = "0.2.3"
6 6 source = "registry+https://github.com/rust-lang/crates.io-index"
7 7 checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
8 8
9 9 [[package]]
10 10 name = "aho-corasick"
11 11 version = "0.7.15"
12 12 source = "registry+https://github.com/rust-lang/crates.io-index"
13 13 checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
14 14 dependencies = [
15 15 "memchr",
16 16 ]
17 17
18 18 [[package]]
19 19 name = "ansi_term"
20 20 version = "0.11.0"
21 21 source = "registry+https://github.com/rust-lang/crates.io-index"
22 22 checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
23 23 dependencies = [
24 24 "winapi",
25 25 ]
26 26
27 27 [[package]]
28 28 name = "atty"
29 29 version = "0.2.14"
30 30 source = "registry+https://github.com/rust-lang/crates.io-index"
31 31 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
32 32 dependencies = [
33 33 "hermit-abi",
34 34 "libc",
35 35 "winapi",
36 36 ]
37 37
38 38 [[package]]
39 39 name = "autocfg"
40 40 version = "1.0.1"
41 41 source = "registry+https://github.com/rust-lang/crates.io-index"
42 42 checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
43 43
44 44 [[package]]
45 45 name = "bitflags"
46 46 version = "1.2.1"
47 47 source = "registry+https://github.com/rust-lang/crates.io-index"
48 48 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
49 49
50 50 [[package]]
51 51 name = "bitmaps"
52 52 version = "2.1.0"
53 53 source = "registry+https://github.com/rust-lang/crates.io-index"
54 54 checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
55 55 dependencies = [
56 56 "typenum",
57 57 ]
58 58
59 59 [[package]]
60 60 name = "byteorder"
61 61 version = "1.3.4"
62 62 source = "registry+https://github.com/rust-lang/crates.io-index"
63 63 checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
64 64
65 65 [[package]]
66 66 name = "bytes-cast"
67 67 version = "0.2.0"
68 68 source = "registry+https://github.com/rust-lang/crates.io-index"
69 69 checksum = "0d434f9a4ecbe987e7ccfda7274b6f82ea52c9b63742565a65cb5e8ba0f2c452"
70 70 dependencies = [
71 71 "bytes-cast-derive",
72 72 ]
73 73
74 74 [[package]]
75 75 name = "bytes-cast-derive"
76 76 version = "0.1.0"
77 77 source = "registry+https://github.com/rust-lang/crates.io-index"
78 78 checksum = "cb936af9de38476664d6b58e529aff30d482e4ce1c5e150293d00730b0d81fdb"
79 79 dependencies = [
80 80 "proc-macro2",
81 81 "quote",
82 82 "syn",
83 83 ]
84 84
85 85 [[package]]
86 86 name = "cc"
87 87 version = "1.0.66"
88 88 source = "registry+https://github.com/rust-lang/crates.io-index"
89 89 checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
90 90 dependencies = [
91 91 "jobserver",
92 92 ]
93 93
94 94 [[package]]
95 95 name = "cfg-if"
96 96 version = "0.1.10"
97 97 source = "registry+https://github.com/rust-lang/crates.io-index"
98 98 checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
99 99
100 100 [[package]]
101 101 name = "cfg-if"
102 102 version = "1.0.0"
103 103 source = "registry+https://github.com/rust-lang/crates.io-index"
104 104 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
105 105
106 106 [[package]]
107 107 name = "chrono"
108 108 version = "0.4.19"
109 109 source = "registry+https://github.com/rust-lang/crates.io-index"
110 110 checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
111 111 dependencies = [
112 112 "libc",
113 113 "num-integer",
114 114 "num-traits",
115 115 "time",
116 116 "winapi",
117 117 ]
118 118
119 119 [[package]]
120 120 name = "clap"
121 121 version = "2.33.3"
122 122 source = "registry+https://github.com/rust-lang/crates.io-index"
123 123 checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
124 124 dependencies = [
125 125 "ansi_term",
126 126 "atty",
127 127 "bitflags",
128 128 "strsim",
129 129 "textwrap",
130 130 "unicode-width",
131 131 "vec_map",
132 132 ]
133 133
134 134 [[package]]
135 135 name = "const_fn"
136 136 version = "0.4.4"
137 137 source = "registry+https://github.com/rust-lang/crates.io-index"
138 138 checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
139 139
140 140 [[package]]
141 141 name = "cpython"
142 142 version = "0.5.2"
143 143 source = "registry+https://github.com/rust-lang/crates.io-index"
144 144 checksum = "0f11357af68648b6a227e7e2384d439cec8595de65970f45e3f7f4b2600be472"
145 145 dependencies = [
146 146 "libc",
147 147 "num-traits",
148 148 "paste",
149 149 "python27-sys",
150 150 "python3-sys",
151 151 ]
152 152
153 153 [[package]]
154 154 name = "crc32fast"
155 155 version = "1.2.1"
156 156 source = "registry+https://github.com/rust-lang/crates.io-index"
157 157 checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
158 158 dependencies = [
159 159 "cfg-if 1.0.0",
160 160 ]
161 161
162 162 [[package]]
163 163 name = "crossbeam-channel"
164 164 version = "0.4.4"
165 165 source = "registry+https://github.com/rust-lang/crates.io-index"
166 166 checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
167 167 dependencies = [
168 168 "crossbeam-utils 0.7.2",
169 169 "maybe-uninit",
170 170 ]
171 171
172 172 [[package]]
173 173 name = "crossbeam-channel"
174 174 version = "0.5.0"
175 175 source = "registry+https://github.com/rust-lang/crates.io-index"
176 176 checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
177 177 dependencies = [
178 178 "cfg-if 1.0.0",
179 179 "crossbeam-utils 0.8.1",
180 180 ]
181 181
182 182 [[package]]
183 183 name = "crossbeam-deque"
184 184 version = "0.8.0"
185 185 source = "registry+https://github.com/rust-lang/crates.io-index"
186 186 checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
187 187 dependencies = [
188 188 "cfg-if 1.0.0",
189 189 "crossbeam-epoch",
190 190 "crossbeam-utils 0.8.1",
191 191 ]
192 192
193 193 [[package]]
194 194 name = "crossbeam-epoch"
195 195 version = "0.9.1"
196 196 source = "registry+https://github.com/rust-lang/crates.io-index"
197 197 checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
198 198 dependencies = [
199 199 "cfg-if 1.0.0",
200 200 "const_fn",
201 201 "crossbeam-utils 0.8.1",
202 202 "lazy_static",
203 203 "memoffset",
204 204 "scopeguard",
205 205 ]
206 206
207 207 [[package]]
208 208 name = "crossbeam-utils"
209 209 version = "0.7.2"
210 210 source = "registry+https://github.com/rust-lang/crates.io-index"
211 211 checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
212 212 dependencies = [
213 213 "autocfg",
214 214 "cfg-if 0.1.10",
215 215 "lazy_static",
216 216 ]
217 217
218 218 [[package]]
219 219 name = "crossbeam-utils"
220 220 version = "0.8.1"
221 221 source = "registry+https://github.com/rust-lang/crates.io-index"
222 222 checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
223 223 dependencies = [
224 224 "autocfg",
225 225 "cfg-if 1.0.0",
226 226 "lazy_static",
227 227 ]
228 228
229 229 [[package]]
230 230 name = "ctor"
231 231 version = "0.1.16"
232 232 source = "registry+https://github.com/rust-lang/crates.io-index"
233 233 checksum = "7fbaabec2c953050352311293be5c6aba8e141ba19d6811862b232d6fd020484"
234 234 dependencies = [
235 235 "quote",
236 236 "syn",
237 237 ]
238 238
239 239 [[package]]
240 240 name = "derive_more"
241 241 version = "0.99.11"
242 242 source = "registry+https://github.com/rust-lang/crates.io-index"
243 243 checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
244 244 dependencies = [
245 245 "proc-macro2",
246 246 "quote",
247 247 "syn",
248 248 ]
249 249
250 250 [[package]]
251 251 name = "difference"
252 252 version = "2.0.0"
253 253 source = "registry+https://github.com/rust-lang/crates.io-index"
254 254 checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
255 255
256 256 [[package]]
257 257 name = "either"
258 258 version = "1.6.1"
259 259 source = "registry+https://github.com/rust-lang/crates.io-index"
260 260 checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
261 261
262 262 [[package]]
263 263 name = "env_logger"
264 264 version = "0.7.1"
265 265 source = "registry+https://github.com/rust-lang/crates.io-index"
266 266 checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
267 267 dependencies = [
268 268 "atty",
269 269 "humantime",
270 270 "log",
271 271 "regex",
272 272 "termcolor",
273 273 ]
274 274
275 275 [[package]]
276 276 name = "flate2"
277 277 version = "1.0.19"
278 278 source = "registry+https://github.com/rust-lang/crates.io-index"
279 279 checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
280 280 dependencies = [
281 281 "cfg-if 1.0.0",
282 282 "crc32fast",
283 283 "libc",
284 284 "libz-sys",
285 285 "miniz_oxide",
286 286 ]
287 287
288 288 [[package]]
289 289 name = "format-bytes"
290 290 version = "0.2.2"
291 291 source = "registry+https://github.com/rust-lang/crates.io-index"
292 292 checksum = "1c4e89040c7fd7b4e6ba2820ac705a45def8a0c098ec78d170ae88f1ef1d5762"
293 293 dependencies = [
294 294 "format-bytes-macros",
295 295 "proc-macro-hack",
296 296 ]
297 297
298 298 [[package]]
299 299 name = "format-bytes-macros"
300 300 version = "0.3.0"
301 301 source = "registry+https://github.com/rust-lang/crates.io-index"
302 302 checksum = "b05089e341a0460449e2210c3bf7b61597860b07f0deae58da38dbed0a4c6b6d"
303 303 dependencies = [
304 304 "proc-macro-hack",
305 305 "proc-macro2",
306 306 "quote",
307 307 "syn",
308 308 ]
309 309
310 310 [[package]]
311 311 name = "fuchsia-cprng"
312 312 version = "0.1.1"
313 313 source = "registry+https://github.com/rust-lang/crates.io-index"
314 314 checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
315 315
316 316 [[package]]
317 317 name = "gcc"
318 318 version = "0.3.55"
319 319 source = "registry+https://github.com/rust-lang/crates.io-index"
320 320 checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
321 321
322 322 [[package]]
323 323 name = "getrandom"
324 324 version = "0.1.15"
325 325 source = "registry+https://github.com/rust-lang/crates.io-index"
326 326 checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
327 327 dependencies = [
328 328 "cfg-if 0.1.10",
329 329 "libc",
330 330 "wasi 0.9.0+wasi-snapshot-preview1",
331 331 ]
332 332
333 333 [[package]]
334 334 name = "glob"
335 335 version = "0.3.0"
336 336 source = "registry+https://github.com/rust-lang/crates.io-index"
337 337 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
338 338
339 339 [[package]]
340 340 name = "hermit-abi"
341 341 version = "0.1.17"
342 342 source = "registry+https://github.com/rust-lang/crates.io-index"
343 343 checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
344 344 dependencies = [
345 345 "libc",
346 346 ]
347 347
348 348 [[package]]
349 349 name = "hg-core"
350 350 version = "0.1.0"
351 351 dependencies = [
352 352 "byteorder",
353 353 "bytes-cast",
354 354 "clap",
355 355 "crossbeam-channel 0.4.4",
356 356 "derive_more",
357 357 "flate2",
358 358 "format-bytes",
359 359 "home",
360 360 "im-rc",
361 "itertools",
361 362 "lazy_static",
362 363 "log",
363 364 "memmap",
364 365 "micro-timer",
365 366 "pretty_assertions",
366 367 "rand 0.7.3",
367 368 "rand_distr",
368 369 "rand_pcg",
369 370 "rayon",
370 371 "regex",
371 372 "rust-crypto",
372 373 "same-file",
373 374 "tempfile",
374 375 "twox-hash",
375 376 "zstd",
376 377 ]
377 378
378 379 [[package]]
379 380 name = "hg-cpython"
380 381 version = "0.1.0"
381 382 dependencies = [
382 383 "cpython",
383 384 "crossbeam-channel 0.4.4",
384 385 "env_logger",
385 386 "hg-core",
386 387 "libc",
387 388 "log",
388 389 ]
389 390
390 391 [[package]]
391 392 name = "home"
392 393 version = "0.5.3"
393 394 source = "registry+https://github.com/rust-lang/crates.io-index"
394 395 checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654"
395 396 dependencies = [
396 397 "winapi",
397 398 ]
398 399
399 400 [[package]]
400 401 name = "humantime"
401 402 version = "1.3.0"
402 403 source = "registry+https://github.com/rust-lang/crates.io-index"
403 404 checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
404 405 dependencies = [
405 406 "quick-error",
406 407 ]
407 408
408 409 [[package]]
409 410 name = "im-rc"
410 411 version = "15.0.0"
411 412 source = "registry+https://github.com/rust-lang/crates.io-index"
412 413 checksum = "3ca8957e71f04a205cb162508f9326aea04676c8dfd0711220190d6b83664f3f"
413 414 dependencies = [
414 415 "bitmaps",
415 416 "rand_core 0.5.1",
416 417 "rand_xoshiro",
417 418 "sized-chunks",
418 419 "typenum",
419 420 "version_check",
420 421 ]
421 422
422 423 [[package]]
423 424 name = "itertools"
424 425 version = "0.9.0"
425 426 source = "registry+https://github.com/rust-lang/crates.io-index"
426 427 checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
427 428 dependencies = [
428 429 "either",
429 430 ]
430 431
431 432 [[package]]
432 433 name = "jobserver"
433 434 version = "0.1.21"
434 435 source = "registry+https://github.com/rust-lang/crates.io-index"
435 436 checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
436 437 dependencies = [
437 438 "libc",
438 439 ]
439 440
440 441 [[package]]
441 442 name = "lazy_static"
442 443 version = "1.4.0"
443 444 source = "registry+https://github.com/rust-lang/crates.io-index"
444 445 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
445 446
446 447 [[package]]
447 448 name = "libc"
448 449 version = "0.2.81"
449 450 source = "registry+https://github.com/rust-lang/crates.io-index"
450 451 checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
451 452
452 453 [[package]]
453 454 name = "libz-sys"
454 455 version = "1.1.2"
455 456 source = "registry+https://github.com/rust-lang/crates.io-index"
456 457 checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
457 458 dependencies = [
458 459 "cc",
459 460 "pkg-config",
460 461 "vcpkg",
461 462 ]
462 463
463 464 [[package]]
464 465 name = "log"
465 466 version = "0.4.11"
466 467 source = "registry+https://github.com/rust-lang/crates.io-index"
467 468 checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
468 469 dependencies = [
469 470 "cfg-if 0.1.10",
470 471 ]
471 472
472 473 [[package]]
473 474 name = "maybe-uninit"
474 475 version = "2.0.0"
475 476 source = "registry+https://github.com/rust-lang/crates.io-index"
476 477 checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
477 478
478 479 [[package]]
479 480 name = "memchr"
480 481 version = "2.3.4"
481 482 source = "registry+https://github.com/rust-lang/crates.io-index"
482 483 checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
483 484
484 485 [[package]]
485 486 name = "memmap"
486 487 version = "0.7.0"
487 488 source = "registry+https://github.com/rust-lang/crates.io-index"
488 489 checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
489 490 dependencies = [
490 491 "libc",
491 492 "winapi",
492 493 ]
493 494
494 495 [[package]]
495 496 name = "memoffset"
496 497 version = "0.6.1"
497 498 source = "registry+https://github.com/rust-lang/crates.io-index"
498 499 checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
499 500 dependencies = [
500 501 "autocfg",
501 502 ]
502 503
503 504 [[package]]
504 505 name = "micro-timer"
505 506 version = "0.3.1"
506 507 source = "registry+https://github.com/rust-lang/crates.io-index"
507 508 checksum = "2620153e1d903d26b72b89f0e9c48d8c4756cba941c185461dddc234980c298c"
508 509 dependencies = [
509 510 "micro-timer-macros",
510 511 "scopeguard",
511 512 ]
512 513
513 514 [[package]]
514 515 name = "micro-timer-macros"
515 516 version = "0.3.1"
516 517 source = "registry+https://github.com/rust-lang/crates.io-index"
517 518 checksum = "e28a3473e6abd6e9aab36aaeef32ad22ae0bd34e79f376643594c2b152ec1c5d"
518 519 dependencies = [
519 520 "proc-macro2",
520 521 "quote",
521 522 "scopeguard",
522 523 "syn",
523 524 ]
524 525
525 526 [[package]]
526 527 name = "miniz_oxide"
527 528 version = "0.4.3"
528 529 source = "registry+https://github.com/rust-lang/crates.io-index"
529 530 checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
530 531 dependencies = [
531 532 "adler",
532 533 "autocfg",
533 534 ]
534 535
535 536 [[package]]
536 537 name = "num-integer"
537 538 version = "0.1.44"
538 539 source = "registry+https://github.com/rust-lang/crates.io-index"
539 540 checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
540 541 dependencies = [
541 542 "autocfg",
542 543 "num-traits",
543 544 ]
544 545
545 546 [[package]]
546 547 name = "num-traits"
547 548 version = "0.2.14"
548 549 source = "registry+https://github.com/rust-lang/crates.io-index"
549 550 checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
550 551 dependencies = [
551 552 "autocfg",
552 553 ]
553 554
554 555 [[package]]
555 556 name = "num_cpus"
556 557 version = "1.13.0"
557 558 source = "registry+https://github.com/rust-lang/crates.io-index"
558 559 checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
559 560 dependencies = [
560 561 "hermit-abi",
561 562 "libc",
562 563 ]
563 564
564 565 [[package]]
565 566 name = "output_vt100"
566 567 version = "0.1.2"
567 568 source = "registry+https://github.com/rust-lang/crates.io-index"
568 569 checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
569 570 dependencies = [
570 571 "winapi",
571 572 ]
572 573
573 574 [[package]]
574 575 name = "paste"
575 576 version = "0.1.18"
576 577 source = "registry+https://github.com/rust-lang/crates.io-index"
577 578 checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880"
578 579 dependencies = [
579 580 "paste-impl",
580 581 "proc-macro-hack",
581 582 ]
582 583
583 584 [[package]]
584 585 name = "paste-impl"
585 586 version = "0.1.18"
586 587 source = "registry+https://github.com/rust-lang/crates.io-index"
587 588 checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6"
588 589 dependencies = [
589 590 "proc-macro-hack",
590 591 ]
591 592
592 593 [[package]]
593 594 name = "pkg-config"
594 595 version = "0.3.19"
595 596 source = "registry+https://github.com/rust-lang/crates.io-index"
596 597 checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
597 598
598 599 [[package]]
599 600 name = "ppv-lite86"
600 601 version = "0.2.10"
601 602 source = "registry+https://github.com/rust-lang/crates.io-index"
602 603 checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
603 604
604 605 [[package]]
605 606 name = "pretty_assertions"
606 607 version = "0.6.1"
607 608 source = "registry+https://github.com/rust-lang/crates.io-index"
608 609 checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
609 610 dependencies = [
610 611 "ansi_term",
611 612 "ctor",
612 613 "difference",
613 614 "output_vt100",
614 615 ]
615 616
616 617 [[package]]
617 618 name = "proc-macro-hack"
618 619 version = "0.5.19"
619 620 source = "registry+https://github.com/rust-lang/crates.io-index"
620 621 checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
621 622
622 623 [[package]]
623 624 name = "proc-macro2"
624 625 version = "1.0.24"
625 626 source = "registry+https://github.com/rust-lang/crates.io-index"
626 627 checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
627 628 dependencies = [
628 629 "unicode-xid",
629 630 ]
630 631
631 632 [[package]]
632 633 name = "python27-sys"
633 634 version = "0.5.2"
634 635 source = "registry+https://github.com/rust-lang/crates.io-index"
635 636 checksum = "f485897ed7048f5032317c4e427800ef9f2053355516524d73952b8b07032054"
636 637 dependencies = [
637 638 "libc",
638 639 "regex",
639 640 ]
640 641
641 642 [[package]]
642 643 name = "python3-sys"
643 644 version = "0.5.2"
644 645 source = "registry+https://github.com/rust-lang/crates.io-index"
645 646 checksum = "5b29b99c6868eb02beb3bf6ed025c8bcdf02efc149b8e80347d3e5d059a806db"
646 647 dependencies = [
647 648 "libc",
648 649 "regex",
649 650 ]
650 651
651 652 [[package]]
652 653 name = "quick-error"
653 654 version = "1.2.3"
654 655 source = "registry+https://github.com/rust-lang/crates.io-index"
655 656 checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
656 657
657 658 [[package]]
658 659 name = "quote"
659 660 version = "1.0.7"
660 661 source = "registry+https://github.com/rust-lang/crates.io-index"
661 662 checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
662 663 dependencies = [
663 664 "proc-macro2",
664 665 ]
665 666
666 667 [[package]]
667 668 name = "rand"
668 669 version = "0.3.23"
669 670 source = "registry+https://github.com/rust-lang/crates.io-index"
670 671 checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
671 672 dependencies = [
672 673 "libc",
673 674 "rand 0.4.6",
674 675 ]
675 676
676 677 [[package]]
677 678 name = "rand"
678 679 version = "0.4.6"
679 680 source = "registry+https://github.com/rust-lang/crates.io-index"
680 681 checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
681 682 dependencies = [
682 683 "fuchsia-cprng",
683 684 "libc",
684 685 "rand_core 0.3.1",
685 686 "rdrand",
686 687 "winapi",
687 688 ]
688 689
689 690 [[package]]
690 691 name = "rand"
691 692 version = "0.7.3"
692 693 source = "registry+https://github.com/rust-lang/crates.io-index"
693 694 checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
694 695 dependencies = [
695 696 "getrandom",
696 697 "libc",
697 698 "rand_chacha",
698 699 "rand_core 0.5.1",
699 700 "rand_hc",
700 701 ]
701 702
702 703 [[package]]
703 704 name = "rand_chacha"
704 705 version = "0.2.2"
705 706 source = "registry+https://github.com/rust-lang/crates.io-index"
706 707 checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
707 708 dependencies = [
708 709 "ppv-lite86",
709 710 "rand_core 0.5.1",
710 711 ]
711 712
712 713 [[package]]
713 714 name = "rand_core"
714 715 version = "0.3.1"
715 716 source = "registry+https://github.com/rust-lang/crates.io-index"
716 717 checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
717 718 dependencies = [
718 719 "rand_core 0.4.2",
719 720 ]
720 721
721 722 [[package]]
722 723 name = "rand_core"
723 724 version = "0.4.2"
724 725 source = "registry+https://github.com/rust-lang/crates.io-index"
725 726 checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
726 727
727 728 [[package]]
728 729 name = "rand_core"
729 730 version = "0.5.1"
730 731 source = "registry+https://github.com/rust-lang/crates.io-index"
731 732 checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
732 733 dependencies = [
733 734 "getrandom",
734 735 ]
735 736
736 737 [[package]]
737 738 name = "rand_distr"
738 739 version = "0.2.2"
739 740 source = "registry+https://github.com/rust-lang/crates.io-index"
740 741 checksum = "96977acbdd3a6576fb1d27391900035bf3863d4a16422973a409b488cf29ffb2"
741 742 dependencies = [
742 743 "rand 0.7.3",
743 744 ]
744 745
745 746 [[package]]
746 747 name = "rand_hc"
747 748 version = "0.2.0"
748 749 source = "registry+https://github.com/rust-lang/crates.io-index"
749 750 checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
750 751 dependencies = [
751 752 "rand_core 0.5.1",
752 753 ]
753 754
754 755 [[package]]
755 756 name = "rand_pcg"
756 757 version = "0.2.1"
757 758 source = "registry+https://github.com/rust-lang/crates.io-index"
758 759 checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
759 760 dependencies = [
760 761 "rand_core 0.5.1",
761 762 ]
762 763
763 764 [[package]]
764 765 name = "rand_xoshiro"
765 766 version = "0.4.0"
766 767 source = "registry+https://github.com/rust-lang/crates.io-index"
767 768 checksum = "a9fcdd2e881d02f1d9390ae47ad8e5696a9e4be7b547a1da2afbc61973217004"
768 769 dependencies = [
769 770 "rand_core 0.5.1",
770 771 ]
771 772
772 773 [[package]]
773 774 name = "rayon"
774 775 version = "1.5.0"
775 776 source = "registry+https://github.com/rust-lang/crates.io-index"
776 777 checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
777 778 dependencies = [
778 779 "autocfg",
779 780 "crossbeam-deque",
780 781 "either",
781 782 "rayon-core",
782 783 ]
783 784
784 785 [[package]]
785 786 name = "rayon-core"
786 787 version = "1.9.0"
787 788 source = "registry+https://github.com/rust-lang/crates.io-index"
788 789 checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
789 790 dependencies = [
790 791 "crossbeam-channel 0.5.0",
791 792 "crossbeam-deque",
792 793 "crossbeam-utils 0.8.1",
793 794 "lazy_static",
794 795 "num_cpus",
795 796 ]
796 797
797 798 [[package]]
798 799 name = "rdrand"
799 800 version = "0.4.0"
800 801 source = "registry+https://github.com/rust-lang/crates.io-index"
801 802 checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
802 803 dependencies = [
803 804 "rand_core 0.3.1",
804 805 ]
805 806
806 807 [[package]]
807 808 name = "redox_syscall"
808 809 version = "0.1.57"
809 810 source = "registry+https://github.com/rust-lang/crates.io-index"
810 811 checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
811 812
812 813 [[package]]
813 814 name = "regex"
814 815 version = "1.4.2"
815 816 source = "registry+https://github.com/rust-lang/crates.io-index"
816 817 checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
817 818 dependencies = [
818 819 "aho-corasick",
819 820 "memchr",
820 821 "regex-syntax",
821 822 "thread_local",
822 823 ]
823 824
824 825 [[package]]
825 826 name = "regex-syntax"
826 827 version = "0.6.21"
827 828 source = "registry+https://github.com/rust-lang/crates.io-index"
828 829 checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
829 830
830 831 [[package]]
831 832 name = "remove_dir_all"
832 833 version = "0.5.3"
833 834 source = "registry+https://github.com/rust-lang/crates.io-index"
834 835 checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
835 836 dependencies = [
836 837 "winapi",
837 838 ]
838 839
839 840 [[package]]
840 841 name = "rhg"
841 842 version = "0.1.0"
842 843 dependencies = [
843 844 "chrono",
844 845 "clap",
845 846 "derive_more",
846 847 "env_logger",
847 848 "format-bytes",
848 849 "hg-core",
849 850 "lazy_static",
850 851 "log",
851 852 "micro-timer",
852 853 "regex",
853 854 "users",
854 855 ]
855 856
856 857 [[package]]
857 858 name = "rust-crypto"
858 859 version = "0.2.36"
859 860 source = "registry+https://github.com/rust-lang/crates.io-index"
860 861 checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
861 862 dependencies = [
862 863 "gcc",
863 864 "libc",
864 865 "rand 0.3.23",
865 866 "rustc-serialize",
866 867 "time",
867 868 ]
868 869
869 870 [[package]]
870 871 name = "rustc-serialize"
871 872 version = "0.3.24"
872 873 source = "registry+https://github.com/rust-lang/crates.io-index"
873 874 checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
874 875
875 876 [[package]]
876 877 name = "same-file"
877 878 version = "1.0.6"
878 879 source = "registry+https://github.com/rust-lang/crates.io-index"
879 880 checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
880 881 dependencies = [
881 882 "winapi-util",
882 883 ]
883 884
884 885 [[package]]
885 886 name = "scopeguard"
886 887 version = "1.1.0"
887 888 source = "registry+https://github.com/rust-lang/crates.io-index"
888 889 checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
889 890
890 891 [[package]]
891 892 name = "sized-chunks"
892 893 version = "0.6.2"
893 894 source = "registry+https://github.com/rust-lang/crates.io-index"
894 895 checksum = "1ec31ceca5644fa6d444cc77548b88b67f46db6f7c71683b0f9336e671830d2f"
895 896 dependencies = [
896 897 "bitmaps",
897 898 "typenum",
898 899 ]
899 900
900 901 [[package]]
901 902 name = "static_assertions"
902 903 version = "1.1.0"
903 904 source = "registry+https://github.com/rust-lang/crates.io-index"
904 905 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
905 906
906 907 [[package]]
907 908 name = "strsim"
908 909 version = "0.8.0"
909 910 source = "registry+https://github.com/rust-lang/crates.io-index"
910 911 checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
911 912
912 913 [[package]]
913 914 name = "syn"
914 915 version = "1.0.54"
915 916 source = "registry+https://github.com/rust-lang/crates.io-index"
916 917 checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44"
917 918 dependencies = [
918 919 "proc-macro2",
919 920 "quote",
920 921 "unicode-xid",
921 922 ]
922 923
923 924 [[package]]
924 925 name = "tempfile"
925 926 version = "3.1.0"
926 927 source = "registry+https://github.com/rust-lang/crates.io-index"
927 928 checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
928 929 dependencies = [
929 930 "cfg-if 0.1.10",
930 931 "libc",
931 932 "rand 0.7.3",
932 933 "redox_syscall",
933 934 "remove_dir_all",
934 935 "winapi",
935 936 ]
936 937
937 938 [[package]]
938 939 name = "termcolor"
939 940 version = "1.1.2"
940 941 source = "registry+https://github.com/rust-lang/crates.io-index"
941 942 checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
942 943 dependencies = [
943 944 "winapi-util",
944 945 ]
945 946
946 947 [[package]]
947 948 name = "textwrap"
948 949 version = "0.11.0"
949 950 source = "registry+https://github.com/rust-lang/crates.io-index"
950 951 checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
951 952 dependencies = [
952 953 "unicode-width",
953 954 ]
954 955
955 956 [[package]]
956 957 name = "thread_local"
957 958 version = "1.0.1"
958 959 source = "registry+https://github.com/rust-lang/crates.io-index"
959 960 checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
960 961 dependencies = [
961 962 "lazy_static",
962 963 ]
963 964
964 965 [[package]]
965 966 name = "time"
966 967 version = "0.1.44"
967 968 source = "registry+https://github.com/rust-lang/crates.io-index"
968 969 checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
969 970 dependencies = [
970 971 "libc",
971 972 "wasi 0.10.0+wasi-snapshot-preview1",
972 973 "winapi",
973 974 ]
974 975
975 976 [[package]]
976 977 name = "twox-hash"
977 978 version = "1.6.0"
978 979 source = "registry+https://github.com/rust-lang/crates.io-index"
979 980 checksum = "04f8ab788026715fa63b31960869617cba39117e520eb415b0139543e325ab59"
980 981 dependencies = [
981 982 "cfg-if 0.1.10",
982 983 "rand 0.7.3",
983 984 "static_assertions",
984 985 ]
985 986
986 987 [[package]]
987 988 name = "typenum"
988 989 version = "1.12.0"
989 990 source = "registry+https://github.com/rust-lang/crates.io-index"
990 991 checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
991 992
992 993 [[package]]
993 994 name = "unicode-width"
994 995 version = "0.1.8"
995 996 source = "registry+https://github.com/rust-lang/crates.io-index"
996 997 checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
997 998
998 999 [[package]]
999 1000 name = "unicode-xid"
1000 1001 version = "0.2.1"
1001 1002 source = "registry+https://github.com/rust-lang/crates.io-index"
1002 1003 checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
1003 1004
1004 1005 [[package]]
1005 1006 name = "users"
1006 1007 version = "0.11.0"
1007 1008 source = "registry+https://github.com/rust-lang/crates.io-index"
1008 1009 checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
1009 1010 dependencies = [
1010 1011 "libc",
1011 1012 "log",
1012 1013 ]
1013 1014
1014 1015 [[package]]
1015 1016 name = "vcpkg"
1016 1017 version = "0.2.11"
1017 1018 source = "registry+https://github.com/rust-lang/crates.io-index"
1018 1019 checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
1019 1020
1020 1021 [[package]]
1021 1022 name = "vec_map"
1022 1023 version = "0.8.2"
1023 1024 source = "registry+https://github.com/rust-lang/crates.io-index"
1024 1025 checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
1025 1026
1026 1027 [[package]]
1027 1028 name = "version_check"
1028 1029 version = "0.9.2"
1029 1030 source = "registry+https://github.com/rust-lang/crates.io-index"
1030 1031 checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
1031 1032
1032 1033 [[package]]
1033 1034 name = "wasi"
1034 1035 version = "0.9.0+wasi-snapshot-preview1"
1035 1036 source = "registry+https://github.com/rust-lang/crates.io-index"
1036 1037 checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
1037 1038
1038 1039 [[package]]
1039 1040 name = "wasi"
1040 1041 version = "0.10.0+wasi-snapshot-preview1"
1041 1042 source = "registry+https://github.com/rust-lang/crates.io-index"
1042 1043 checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
1043 1044
1044 1045 [[package]]
1045 1046 name = "winapi"
1046 1047 version = "0.3.9"
1047 1048 source = "registry+https://github.com/rust-lang/crates.io-index"
1048 1049 checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1049 1050 dependencies = [
1050 1051 "winapi-i686-pc-windows-gnu",
1051 1052 "winapi-x86_64-pc-windows-gnu",
1052 1053 ]
1053 1054
1054 1055 [[package]]
1055 1056 name = "winapi-i686-pc-windows-gnu"
1056 1057 version = "0.4.0"
1057 1058 source = "registry+https://github.com/rust-lang/crates.io-index"
1058 1059 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1059 1060
1060 1061 [[package]]
1061 1062 name = "winapi-util"
1062 1063 version = "0.1.5"
1063 1064 source = "registry+https://github.com/rust-lang/crates.io-index"
1064 1065 checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
1065 1066 dependencies = [
1066 1067 "winapi",
1067 1068 ]
1068 1069
1069 1070 [[package]]
1070 1071 name = "winapi-x86_64-pc-windows-gnu"
1071 1072 version = "0.4.0"
1072 1073 source = "registry+https://github.com/rust-lang/crates.io-index"
1073 1074 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1074 1075
1075 1076 [[package]]
1076 1077 name = "zstd"
1077 1078 version = "0.5.3+zstd.1.4.5"
1078 1079 source = "registry+https://github.com/rust-lang/crates.io-index"
1079 1080 checksum = "01b32eaf771efa709e8308605bbf9319bf485dc1503179ec0469b611937c0cd8"
1080 1081 dependencies = [
1081 1082 "zstd-safe",
1082 1083 ]
1083 1084
1084 1085 [[package]]
1085 1086 name = "zstd-safe"
1086 1087 version = "2.0.5+zstd.1.4.5"
1087 1088 source = "registry+https://github.com/rust-lang/crates.io-index"
1088 1089 checksum = "1cfb642e0d27f64729a639c52db457e0ae906e7bc6f5fe8f5c453230400f1055"
1089 1090 dependencies = [
1090 1091 "libc",
1091 1092 "zstd-sys",
1092 1093 ]
1093 1094
1094 1095 [[package]]
1095 1096 name = "zstd-sys"
1096 1097 version = "1.4.17+zstd.1.4.5"
1097 1098 source = "registry+https://github.com/rust-lang/crates.io-index"
1098 1099 checksum = "b89249644df056b522696b1bb9e7c18c87e8ffa3e2f0dc3b0155875d6498f01b"
1099 1100 dependencies = [
1100 1101 "cc",
1101 1102 "glob",
1102 1103 "itertools",
1103 1104 "libc",
1104 1105 ]
@@ -1,43 +1,44 b''
1 1 [package]
2 2 name = "hg-core"
3 3 version = "0.1.0"
4 4 authors = ["Georges Racinet <gracinet@anybox.fr>"]
5 5 description = "Mercurial pure Rust core library, with no assumption on Python bindings (FFI)"
6 6 edition = "2018"
7 7
8 8 [lib]
9 9 name = "hg"
10 10
11 11 [dependencies]
12 12 bytes-cast = "0.2"
13 13 byteorder = "1.3.4"
14 14 derive_more = "0.99"
15 15 home = "0.5"
16 16 im-rc = "15.0.*"
17 itertools = "0.9"
17 18 lazy_static = "1.4.0"
18 19 rand = "0.7.3"
19 20 rand_pcg = "0.2.1"
20 21 rand_distr = "0.2.2"
21 22 rayon = "1.3.0"
22 23 regex = "1.3.9"
23 24 twox-hash = "1.5.0"
24 25 same-file = "1.0.6"
25 26 crossbeam-channel = "0.4"
26 27 micro-timer = "0.3.0"
27 28 log = "0.4.8"
28 29 memmap = "0.7.0"
29 30 zstd = "0.5.3"
30 31 rust-crypto = "0.2.36"
31 32 format-bytes = "0.2.2"
32 33
33 34 # We don't use the `miniz-oxide` backend to not change rhg benchmarks and until
34 35 # we have a clearer view of which backend is the fastest.
35 36 [dependencies.flate2]
36 37 version = "1.0.16"
37 38 features = ["zlib"]
38 39 default-features = false
39 40
40 41 [dev-dependencies]
41 42 clap = "*"
42 43 pretty_assertions = "0.6.1"
43 44 tempfile = "3.1.0"
@@ -1,119 +1,132 b''
1 1 // dirstate module
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use crate::errors::HgError;
9 9 use crate::revlog::Node;
10 10 use crate::{utils::hg_path::HgPathBuf, FastHashMap};
11 11 use bytes_cast::{unaligned, BytesCast};
12 12 use std::convert::TryFrom;
13 13
14 14 pub mod dirs_multiset;
15 15 pub mod dirstate_map;
16 16 pub mod parsers;
17 17 pub mod status;
18 18
19 19 #[derive(Debug, PartialEq, Clone, BytesCast)]
20 20 #[repr(C)]
21 21 pub struct DirstateParents {
22 22 pub p1: Node,
23 23 pub p2: Node,
24 24 }
25 25
26 26 /// The C implementation uses all signed types. This will be an issue
27 27 /// either when 4GB+ source files are commonplace or in 2038, whichever
28 28 /// comes first.
29 29 #[derive(Debug, PartialEq, Copy, Clone)]
30 30 pub struct DirstateEntry {
31 31 pub state: EntryState,
32 32 pub mode: i32,
33 33 pub mtime: i32,
34 34 pub size: i32,
35 35 }
36 36
37 37 impl DirstateEntry {
38 38 pub fn is_non_normal(&self) -> bool {
39 39 self.state != EntryState::Normal || self.mtime == MTIME_UNSET
40 40 }
41 41
42 42 pub fn is_from_other_parent(&self) -> bool {
43 43 self.state == EntryState::Normal && self.size == SIZE_FROM_OTHER_PARENT
44 44 }
45
46 // TODO: other platforms
47 #[cfg(unix)]
48 pub fn mode_changed(
49 &self,
50 filesystem_metadata: &std::fs::Metadata,
51 ) -> bool {
52 use std::os::unix::fs::MetadataExt;
53 const EXEC_BIT_MASK: u32 = 0o100;
54 let dirstate_exec_bit = (self.mode as u32) & EXEC_BIT_MASK;
55 let fs_exec_bit = filesystem_metadata.mode() & EXEC_BIT_MASK;
56 dirstate_exec_bit != fs_exec_bit
57 }
45 58 }
46 59
47 60 #[derive(BytesCast)]
48 61 #[repr(C)]
49 62 struct RawEntry {
50 63 state: u8,
51 64 mode: unaligned::I32Be,
52 65 size: unaligned::I32Be,
53 66 mtime: unaligned::I32Be,
54 67 length: unaligned::I32Be,
55 68 }
56 69
57 70 const MTIME_UNSET: i32 = -1;
58 71
59 72 /// A `DirstateEntry` with a size of `-2` means that it was merged from the
60 73 /// other parent. This allows revert to pick the right status back during a
61 74 /// merge.
62 75 pub const SIZE_FROM_OTHER_PARENT: i32 = -2;
63 76
64 77 pub type StateMap = FastHashMap<HgPathBuf, DirstateEntry>;
65 78 pub type StateMapIter<'a> =
66 79 Box<dyn Iterator<Item = (&'a HgPathBuf, &'a DirstateEntry)> + Send + 'a>;
67 80
68 81 pub type CopyMap = FastHashMap<HgPathBuf, HgPathBuf>;
69 82 pub type CopyMapIter<'a> =
70 83 Box<dyn Iterator<Item = (&'a HgPathBuf, &'a HgPathBuf)> + Send + 'a>;
71 84
72 85 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
73 86 pub enum EntryState {
74 87 Normal,
75 88 Added,
76 89 Removed,
77 90 Merged,
78 91 Unknown,
79 92 }
80 93
81 94 impl EntryState {
82 95 pub fn is_tracked(self) -> bool {
83 96 use EntryState::*;
84 97 match self {
85 98 Normal | Added | Merged => true,
86 99 Removed | Unknown => false,
87 100 }
88 101 }
89 102 }
90 103
91 104 impl TryFrom<u8> for EntryState {
92 105 type Error = HgError;
93 106
94 107 fn try_from(value: u8) -> Result<Self, Self::Error> {
95 108 match value {
96 109 b'n' => Ok(EntryState::Normal),
97 110 b'a' => Ok(EntryState::Added),
98 111 b'r' => Ok(EntryState::Removed),
99 112 b'm' => Ok(EntryState::Merged),
100 113 b'?' => Ok(EntryState::Unknown),
101 114 _ => Err(HgError::CorruptedRepository(format!(
102 115 "Incorrect dirstate entry state {}",
103 116 value
104 117 ))),
105 118 }
106 119 }
107 120 }
108 121
109 122 impl Into<u8> for EntryState {
110 123 fn into(self) -> u8 {
111 124 match self {
112 125 EntryState::Normal => b'n',
113 126 EntryState::Added => b'a',
114 127 EntryState::Removed => b'r',
115 128 EntryState::Merged => b'm',
116 129 EntryState::Unknown => b'?',
117 130 }
118 131 }
119 132 }
@@ -1,934 +1,934 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 //! Rust implementation of dirstate.status (dirstate.py).
9 9 //! It is currently missing a lot of functionality compared to the Python one
10 10 //! and will only be triggered in narrow cases.
11 11
12 12 use crate::utils::path_auditor::PathAuditor;
13 13 use crate::{
14 14 dirstate::SIZE_FROM_OTHER_PARENT,
15 15 filepatterns::PatternFileWarning,
16 16 matchers::{get_ignore_function, Matcher, VisitChildrenSet},
17 17 utils::{
18 18 files::{find_dirs, HgMetadata},
19 19 hg_path::{
20 20 hg_path_to_path_buf, os_string_to_hg_path_buf, HgPath, HgPathBuf,
21 21 HgPathError,
22 22 },
23 23 },
24 24 CopyMap, DirstateEntry, DirstateMap, EntryState, FastHashMap,
25 25 PatternError,
26 26 };
27 27 use lazy_static::lazy_static;
28 28 use micro_timer::timed;
29 29 use rayon::prelude::*;
30 30 use std::{
31 31 borrow::Cow,
32 32 collections::HashSet,
33 33 fmt,
34 34 fs::{read_dir, DirEntry},
35 35 io::ErrorKind,
36 36 ops::Deref,
37 37 path::{Path, PathBuf},
38 38 };
39 39
40 40 /// Wrong type of file from a `BadMatch`
41 41 /// Note: a lot of those don't exist on all platforms.
42 42 #[derive(Debug, Copy, Clone)]
43 43 pub enum BadType {
44 44 CharacterDevice,
45 45 BlockDevice,
46 46 FIFO,
47 47 Socket,
48 48 Directory,
49 49 Unknown,
50 50 }
51 51
52 52 impl fmt::Display for BadType {
53 53 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54 54 f.write_str(match self {
55 55 BadType::CharacterDevice => "character device",
56 56 BadType::BlockDevice => "block device",
57 57 BadType::FIFO => "fifo",
58 58 BadType::Socket => "socket",
59 59 BadType::Directory => "directory",
60 60 BadType::Unknown => "unknown",
61 61 })
62 62 }
63 63 }
64 64
65 65 /// Was explicitly matched but cannot be found/accessed
66 66 #[derive(Debug, Copy, Clone)]
67 67 pub enum BadMatch {
68 68 OsError(i32),
69 69 BadType(BadType),
70 70 }
71 71
72 72 /// Enum used to dispatch new status entries into the right collections.
73 73 /// Is similar to `crate::EntryState`, but represents the transient state of
74 74 /// entries during the lifetime of a command.
75 75 #[derive(Debug, Copy, Clone)]
76 76 pub enum Dispatch {
77 77 Unsure,
78 78 Modified,
79 79 Added,
80 80 Removed,
81 81 Deleted,
82 82 Clean,
83 83 Unknown,
84 84 Ignored,
85 85 /// Empty dispatch, the file is not worth listing
86 86 None,
87 87 /// Was explicitly matched but cannot be found/accessed
88 88 Bad(BadMatch),
89 89 Directory {
90 90 /// True if the directory used to be a file in the dmap so we can say
91 91 /// that it's been removed.
92 92 was_file: bool,
93 93 },
94 94 }
95 95
96 96 type IoResult<T> = std::io::Result<T>;
97 97
98 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait, 'static>`, so add
98 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
99 99 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
100 100 pub type IgnoreFnType<'a> =
101 101 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
102 102
103 103 /// We have a good mix of owned (from directory traversal) and borrowed (from
104 104 /// the dirstate/explicit) paths, this comes up a lot.
105 105 pub type HgPathCow<'a> = Cow<'a, HgPath>;
106 106
107 107 /// A path with its computed ``Dispatch`` information
108 108 type DispatchedPath<'a> = (HgPathCow<'a>, Dispatch);
109 109
110 110 /// The conversion from `HgPath` to a real fs path failed.
111 111 /// `22` is the error code for "Invalid argument"
112 112 const INVALID_PATH_DISPATCH: Dispatch = Dispatch::Bad(BadMatch::OsError(22));
113 113
114 114 /// Dates and times that are outside the 31-bit signed range are compared
115 115 /// modulo 2^31. This should prevent hg from behaving badly with very large
116 116 /// files or corrupt dates while still having a high probability of detecting
117 117 /// changes. (issue2608)
118 118 /// TODO I haven't found a way of having `b` be `Into<i32>`, since `From<u64>`
119 119 /// is not defined for `i32`, and there is no `As` trait. This forces the
120 120 /// caller to cast `b` as `i32`.
121 121 fn mod_compare(a: i32, b: i32) -> bool {
122 122 a & i32::max_value() != b & i32::max_value()
123 123 }
124 124
125 125 /// Return a sorted list containing information about the entries
126 126 /// in the directory.
127 127 ///
128 128 /// * `skip_dot_hg` - Return an empty vec if `path` contains a `.hg` directory
129 129 fn list_directory(
130 130 path: impl AsRef<Path>,
131 131 skip_dot_hg: bool,
132 132 ) -> std::io::Result<Vec<(HgPathBuf, DirEntry)>> {
133 133 let mut results = vec![];
134 134 let entries = read_dir(path.as_ref())?;
135 135
136 136 for entry in entries {
137 137 let entry = entry?;
138 138 let filename = os_string_to_hg_path_buf(entry.file_name())?;
139 139 let file_type = entry.file_type()?;
140 140 if skip_dot_hg && filename.as_bytes() == b".hg" && file_type.is_dir() {
141 141 return Ok(vec![]);
142 142 } else {
143 143 results.push((filename, entry))
144 144 }
145 145 }
146 146
147 147 results.sort_unstable_by_key(|e| e.0.clone());
148 148 Ok(results)
149 149 }
150 150
151 151 /// The file corresponding to the dirstate entry was found on the filesystem.
152 152 fn dispatch_found(
153 153 filename: impl AsRef<HgPath>,
154 154 entry: DirstateEntry,
155 155 metadata: HgMetadata,
156 156 copy_map: &CopyMap,
157 157 options: StatusOptions,
158 158 ) -> Dispatch {
159 159 let DirstateEntry {
160 160 state,
161 161 mode,
162 162 mtime,
163 163 size,
164 164 } = entry;
165 165
166 166 let HgMetadata {
167 167 st_mode,
168 168 st_size,
169 169 st_mtime,
170 170 ..
171 171 } = metadata;
172 172
173 173 match state {
174 174 EntryState::Normal => {
175 175 let size_changed = mod_compare(size, st_size as i32);
176 176 let mode_changed =
177 177 (mode ^ st_mode as i32) & 0o100 != 0o000 && options.check_exec;
178 178 let metadata_changed = size >= 0 && (size_changed || mode_changed);
179 179 let other_parent = size == SIZE_FROM_OTHER_PARENT;
180 180
181 181 if metadata_changed
182 182 || other_parent
183 183 || copy_map.contains_key(filename.as_ref())
184 184 {
185 185 if metadata.is_symlink() && size_changed {
186 186 // issue6456: Size returned may be longer due to encryption
187 187 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
188 188 Dispatch::Unsure
189 189 } else {
190 190 Dispatch::Modified
191 191 }
192 192 } else if mod_compare(mtime, st_mtime as i32)
193 193 || st_mtime == options.last_normal_time
194 194 {
195 195 // the file may have just been marked as normal and
196 196 // it may have changed in the same second without
197 197 // changing its size. This can happen if we quickly
198 198 // do multiple commits. Force lookup, so we don't
199 199 // miss such a racy file change.
200 200 Dispatch::Unsure
201 201 } else if options.list_clean {
202 202 Dispatch::Clean
203 203 } else {
204 204 Dispatch::None
205 205 }
206 206 }
207 207 EntryState::Merged => Dispatch::Modified,
208 208 EntryState::Added => Dispatch::Added,
209 209 EntryState::Removed => Dispatch::Removed,
210 210 EntryState::Unknown => Dispatch::Unknown,
211 211 }
212 212 }
213 213
214 214 /// The file corresponding to this Dirstate entry is missing.
215 215 fn dispatch_missing(state: EntryState) -> Dispatch {
216 216 match state {
217 217 // File was removed from the filesystem during commands
218 218 EntryState::Normal | EntryState::Merged | EntryState::Added => {
219 219 Dispatch::Deleted
220 220 }
221 221 // File was removed, everything is normal
222 222 EntryState::Removed => Dispatch::Removed,
223 223 // File is unknown to Mercurial, everything is normal
224 224 EntryState::Unknown => Dispatch::Unknown,
225 225 }
226 226 }
227 227
228 228 fn dispatch_os_error(e: &std::io::Error) -> Dispatch {
229 229 Dispatch::Bad(BadMatch::OsError(
230 230 e.raw_os_error().expect("expected real OS error"),
231 231 ))
232 232 }
233 233
234 234 lazy_static! {
235 235 static ref DEFAULT_WORK: HashSet<&'static HgPath> = {
236 236 let mut h = HashSet::new();
237 237 h.insert(HgPath::new(b""));
238 238 h
239 239 };
240 240 }
241 241
242 242 #[derive(Debug, Copy, Clone)]
243 243 pub struct StatusOptions {
244 244 /// Remember the most recent modification timeslot for status, to make
245 245 /// sure we won't miss future size-preserving file content modifications
246 246 /// that happen within the same timeslot.
247 247 pub last_normal_time: i64,
248 248 /// Whether we are on a filesystem with UNIX-like exec flags
249 249 pub check_exec: bool,
250 250 pub list_clean: bool,
251 251 pub list_unknown: bool,
252 252 pub list_ignored: bool,
253 253 /// Whether to collect traversed dirs for applying a callback later.
254 254 /// Used by `hg purge` for example.
255 255 pub collect_traversed_dirs: bool,
256 256 }
257 257
258 #[derive(Debug)]
258 #[derive(Debug, Default)]
259 259 pub struct DirstateStatus<'a> {
260 260 /// Tracked files whose contents have changed since the parent revision
261 261 pub modified: Vec<HgPathCow<'a>>,
262 262
263 263 /// Newly-tracked files that were not present in the parent
264 264 pub added: Vec<HgPathCow<'a>>,
265 265
266 266 /// Previously-tracked files that have been (re)moved with an hg command
267 267 pub removed: Vec<HgPathCow<'a>>,
268 268
269 269 /// (Still) tracked files that are missing, (re)moved with an non-hg
270 270 /// command
271 271 pub deleted: Vec<HgPathCow<'a>>,
272 272
273 273 /// Tracked files that are up to date with the parent.
274 274 /// Only pupulated if `StatusOptions::list_clean` is true.
275 275 pub clean: Vec<HgPathCow<'a>>,
276 276
277 277 /// Files in the working directory that are ignored with `.hgignore`.
278 278 /// Only pupulated if `StatusOptions::list_ignored` is true.
279 279 pub ignored: Vec<HgPathCow<'a>>,
280 280
281 281 /// Files in the working directory that are neither tracked nor ignored.
282 282 /// Only pupulated if `StatusOptions::list_unknown` is true.
283 283 pub unknown: Vec<HgPathCow<'a>>,
284 284
285 285 /// Was explicitly matched but cannot be found/accessed
286 286 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
287 287
288 288 /// Either clean or modified, but we can’t tell from filesystem metadata
289 289 /// alone. The file contents need to be read and compared with that in
290 290 /// the parent.
291 291 pub unsure: Vec<HgPathCow<'a>>,
292 292
293 293 /// Only filled if `collect_traversed_dirs` is `true`
294 294 pub traversed: Vec<HgPathBuf>,
295 295 }
296 296
297 297 #[derive(Debug, derive_more::From)]
298 298 pub enum StatusError {
299 299 /// Generic IO error
300 300 IO(std::io::Error),
301 301 /// An invalid path that cannot be represented in Mercurial was found
302 302 Path(HgPathError),
303 303 /// An invalid "ignore" pattern was found
304 304 Pattern(PatternError),
305 305 }
306 306
307 307 pub type StatusResult<T> = Result<T, StatusError>;
308 308
309 309 impl fmt::Display for StatusError {
310 310 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
311 311 match self {
312 312 StatusError::IO(error) => error.fmt(f),
313 313 StatusError::Path(error) => error.fmt(f),
314 314 StatusError::Pattern(error) => error.fmt(f),
315 315 }
316 316 }
317 317 }
318 318
319 319 /// Gives information about which files are changed in the working directory
320 320 /// and how, compared to the revision we're based on
321 321 pub struct Status<'a, M: ?Sized + Matcher + Sync> {
322 322 dmap: &'a DirstateMap,
323 323 pub(crate) matcher: &'a M,
324 324 root_dir: PathBuf,
325 325 pub(crate) options: StatusOptions,
326 326 ignore_fn: IgnoreFnType<'a>,
327 327 }
328 328
329 329 impl<'a, M> Status<'a, M>
330 330 where
331 331 M: ?Sized + Matcher + Sync,
332 332 {
333 333 pub fn new(
334 334 dmap: &'a DirstateMap,
335 335 matcher: &'a M,
336 336 root_dir: PathBuf,
337 337 ignore_files: Vec<PathBuf>,
338 338 options: StatusOptions,
339 339 ) -> StatusResult<(Self, Vec<PatternFileWarning>)> {
340 340 // Needs to outlive `dir_ignore_fn` since it's captured.
341 341
342 342 let (ignore_fn, warnings): (IgnoreFnType, _) =
343 343 if options.list_ignored || options.list_unknown {
344 344 get_ignore_function(ignore_files, &root_dir)?
345 345 } else {
346 346 (Box::new(|&_| true), vec![])
347 347 };
348 348
349 349 Ok((
350 350 Self {
351 351 dmap,
352 352 matcher,
353 353 root_dir,
354 354 options,
355 355 ignore_fn,
356 356 },
357 357 warnings,
358 358 ))
359 359 }
360 360
361 361 /// Is the path ignored?
362 362 pub fn is_ignored(&self, path: impl AsRef<HgPath>) -> bool {
363 363 (self.ignore_fn)(path.as_ref())
364 364 }
365 365
366 366 /// Is the path or one of its ancestors ignored?
367 367 pub fn dir_ignore(&self, dir: impl AsRef<HgPath>) -> bool {
368 368 // Only involve ignore mechanism if we're listing unknowns or ignored.
369 369 if self.options.list_ignored || self.options.list_unknown {
370 370 if self.is_ignored(&dir) {
371 371 true
372 372 } else {
373 373 for p in find_dirs(dir.as_ref()) {
374 374 if self.is_ignored(p) {
375 375 return true;
376 376 }
377 377 }
378 378 false
379 379 }
380 380 } else {
381 381 true
382 382 }
383 383 }
384 384
385 385 /// Get stat data about the files explicitly specified by the matcher.
386 386 /// Returns a tuple of the directories that need to be traversed and the
387 387 /// files with their corresponding `Dispatch`.
388 388 /// TODO subrepos
389 389 #[timed]
390 390 pub fn walk_explicit(
391 391 &self,
392 392 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
393 393 ) -> (Vec<DispatchedPath<'a>>, Vec<DispatchedPath<'a>>) {
394 394 self.matcher
395 395 .file_set()
396 396 .unwrap_or(&DEFAULT_WORK)
397 397 .par_iter()
398 398 .flat_map(|&filename| -> Option<_> {
399 399 // TODO normalization
400 400 let normalized = filename;
401 401
402 402 let buf = match hg_path_to_path_buf(normalized) {
403 403 Ok(x) => x,
404 404 Err(_) => {
405 405 return Some((
406 406 Cow::Borrowed(normalized),
407 407 INVALID_PATH_DISPATCH,
408 408 ))
409 409 }
410 410 };
411 411 let target = self.root_dir.join(buf);
412 412 let st = target.symlink_metadata();
413 413 let in_dmap = self.dmap.get(normalized);
414 414 match st {
415 415 Ok(meta) => {
416 416 let file_type = meta.file_type();
417 417 return if file_type.is_file() || file_type.is_symlink()
418 418 {
419 419 if let Some(entry) = in_dmap {
420 420 return Some((
421 421 Cow::Borrowed(normalized),
422 422 dispatch_found(
423 423 &normalized,
424 424 *entry,
425 425 HgMetadata::from_metadata(meta),
426 426 &self.dmap.copy_map,
427 427 self.options,
428 428 ),
429 429 ));
430 430 }
431 431 Some((
432 432 Cow::Borrowed(normalized),
433 433 Dispatch::Unknown,
434 434 ))
435 435 } else if file_type.is_dir() {
436 436 if self.options.collect_traversed_dirs {
437 437 traversed_sender
438 438 .send(normalized.to_owned())
439 439 .expect("receiver should outlive sender");
440 440 }
441 441 Some((
442 442 Cow::Borrowed(normalized),
443 443 Dispatch::Directory {
444 444 was_file: in_dmap.is_some(),
445 445 },
446 446 ))
447 447 } else {
448 448 Some((
449 449 Cow::Borrowed(normalized),
450 450 Dispatch::Bad(BadMatch::BadType(
451 451 // TODO do more than unknown
452 452 // Support for all `BadType` variant
453 453 // varies greatly between platforms.
454 454 // So far, no tests check the type and
455 455 // this should be good enough for most
456 456 // users.
457 457 BadType::Unknown,
458 458 )),
459 459 ))
460 460 };
461 461 }
462 462 Err(_) => {
463 463 if let Some(entry) = in_dmap {
464 464 return Some((
465 465 Cow::Borrowed(normalized),
466 466 dispatch_missing(entry.state),
467 467 ));
468 468 }
469 469 }
470 470 };
471 471 None
472 472 })
473 473 .partition(|(_, dispatch)| match dispatch {
474 474 Dispatch::Directory { .. } => true,
475 475 _ => false,
476 476 })
477 477 }
478 478
479 479 /// Walk the working directory recursively to look for changes compared to
480 480 /// the current `DirstateMap`.
481 481 ///
482 482 /// This takes a mutable reference to the results to account for the
483 483 /// `extend` in timings
484 484 #[timed]
485 485 pub fn traverse(
486 486 &self,
487 487 path: impl AsRef<HgPath>,
488 488 old_results: &FastHashMap<HgPathCow<'a>, Dispatch>,
489 489 results: &mut Vec<DispatchedPath<'a>>,
490 490 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
491 491 ) {
492 492 // The traversal is done in parallel, so use a channel to gather
493 493 // entries. `crossbeam_channel::Sender` is `Sync`, while `mpsc::Sender`
494 494 // is not.
495 495 let (files_transmitter, files_receiver) =
496 496 crossbeam_channel::unbounded();
497 497
498 498 self.traverse_dir(
499 499 &files_transmitter,
500 500 path,
501 501 &old_results,
502 502 traversed_sender,
503 503 );
504 504
505 505 // Disconnect the channel so the receiver stops waiting
506 506 drop(files_transmitter);
507 507
508 508 let new_results = files_receiver
509 509 .into_iter()
510 510 .par_bridge()
511 511 .map(|(f, d)| (Cow::Owned(f), d));
512 512
513 513 results.par_extend(new_results);
514 514 }
515 515
516 516 /// Dispatch a single entry (file, folder, symlink...) found during
517 517 /// `traverse`. If the entry is a folder that needs to be traversed, it
518 518 /// will be handled in a separate thread.
519 519 fn handle_traversed_entry<'b>(
520 520 &'a self,
521 521 scope: &rayon::Scope<'b>,
522 522 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
523 523 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
524 524 filename: HgPathBuf,
525 525 dir_entry: DirEntry,
526 526 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
527 527 ) -> IoResult<()>
528 528 where
529 529 'a: 'b,
530 530 {
531 531 let file_type = dir_entry.file_type()?;
532 532 let entry_option = self.dmap.get(&filename);
533 533
534 534 if filename.as_bytes() == b".hg" {
535 535 // Could be a directory or a symlink
536 536 return Ok(());
537 537 }
538 538
539 539 if file_type.is_dir() {
540 540 self.handle_traversed_dir(
541 541 scope,
542 542 files_sender,
543 543 old_results,
544 544 entry_option,
545 545 filename,
546 546 traversed_sender,
547 547 );
548 548 } else if file_type.is_file() || file_type.is_symlink() {
549 549 if let Some(entry) = entry_option {
550 550 if self.matcher.matches_everything()
551 551 || self.matcher.matches(&filename)
552 552 {
553 553 let metadata = dir_entry.metadata()?;
554 554 files_sender
555 555 .send((
556 556 filename.to_owned(),
557 557 dispatch_found(
558 558 &filename,
559 559 *entry,
560 560 HgMetadata::from_metadata(metadata),
561 561 &self.dmap.copy_map,
562 562 self.options,
563 563 ),
564 564 ))
565 565 .unwrap();
566 566 }
567 567 } else if (self.matcher.matches_everything()
568 568 || self.matcher.matches(&filename))
569 569 && !self.is_ignored(&filename)
570 570 {
571 571 if (self.options.list_ignored
572 572 || self.matcher.exact_match(&filename))
573 573 && self.dir_ignore(&filename)
574 574 {
575 575 if self.options.list_ignored {
576 576 files_sender
577 577 .send((filename.to_owned(), Dispatch::Ignored))
578 578 .unwrap();
579 579 }
580 580 } else if self.options.list_unknown {
581 581 files_sender
582 582 .send((filename.to_owned(), Dispatch::Unknown))
583 583 .unwrap();
584 584 }
585 585 } else if self.is_ignored(&filename) && self.options.list_ignored {
586 586 files_sender
587 587 .send((filename.to_owned(), Dispatch::Ignored))
588 588 .unwrap();
589 589 }
590 590 } else if let Some(entry) = entry_option {
591 591 // Used to be a file or a folder, now something else.
592 592 if self.matcher.matches_everything()
593 593 || self.matcher.matches(&filename)
594 594 {
595 595 files_sender
596 596 .send((filename.to_owned(), dispatch_missing(entry.state)))
597 597 .unwrap();
598 598 }
599 599 }
600 600
601 601 Ok(())
602 602 }
603 603
604 604 /// A directory was found in the filesystem and needs to be traversed
605 605 fn handle_traversed_dir<'b>(
606 606 &'a self,
607 607 scope: &rayon::Scope<'b>,
608 608 files_sender: &'b crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
609 609 old_results: &'a FastHashMap<Cow<HgPath>, Dispatch>,
610 610 entry_option: Option<&'a DirstateEntry>,
611 611 directory: HgPathBuf,
612 612 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
613 613 ) where
614 614 'a: 'b,
615 615 {
616 616 scope.spawn(move |_| {
617 617 // Nested `if` until `rust-lang/rust#53668` is stable
618 618 if let Some(entry) = entry_option {
619 619 // Used to be a file, is now a folder
620 620 if self.matcher.matches_everything()
621 621 || self.matcher.matches(&directory)
622 622 {
623 623 files_sender
624 624 .send((
625 625 directory.to_owned(),
626 626 dispatch_missing(entry.state),
627 627 ))
628 628 .unwrap();
629 629 }
630 630 }
631 631 // Do we need to traverse it?
632 632 if !self.is_ignored(&directory) || self.options.list_ignored {
633 633 self.traverse_dir(
634 634 files_sender,
635 635 directory,
636 636 &old_results,
637 637 traversed_sender,
638 638 )
639 639 }
640 640 });
641 641 }
642 642
643 643 /// Decides whether the directory needs to be listed, and if so handles the
644 644 /// entries in a separate thread.
645 645 fn traverse_dir(
646 646 &self,
647 647 files_sender: &crossbeam_channel::Sender<(HgPathBuf, Dispatch)>,
648 648 directory: impl AsRef<HgPath>,
649 649 old_results: &FastHashMap<Cow<HgPath>, Dispatch>,
650 650 traversed_sender: crossbeam_channel::Sender<HgPathBuf>,
651 651 ) {
652 652 let directory = directory.as_ref();
653 653
654 654 if self.options.collect_traversed_dirs {
655 655 traversed_sender
656 656 .send(directory.to_owned())
657 657 .expect("receiver should outlive sender");
658 658 }
659 659
660 660 let visit_entries = match self.matcher.visit_children_set(directory) {
661 661 VisitChildrenSet::Empty => return,
662 662 VisitChildrenSet::This | VisitChildrenSet::Recursive => None,
663 663 VisitChildrenSet::Set(set) => Some(set),
664 664 };
665 665 let buf = match hg_path_to_path_buf(directory) {
666 666 Ok(b) => b,
667 667 Err(_) => {
668 668 files_sender
669 669 .send((directory.to_owned(), INVALID_PATH_DISPATCH))
670 670 .expect("receiver should outlive sender");
671 671 return;
672 672 }
673 673 };
674 674 let dir_path = self.root_dir.join(buf);
675 675
676 676 let skip_dot_hg = !directory.as_bytes().is_empty();
677 677 let entries = match list_directory(dir_path, skip_dot_hg) {
678 678 Err(e) => {
679 679 files_sender
680 680 .send((directory.to_owned(), dispatch_os_error(&e)))
681 681 .expect("receiver should outlive sender");
682 682 return;
683 683 }
684 684 Ok(entries) => entries,
685 685 };
686 686
687 687 rayon::scope(|scope| {
688 688 for (filename, dir_entry) in entries {
689 689 if let Some(ref set) = visit_entries {
690 690 if !set.contains(filename.deref()) {
691 691 continue;
692 692 }
693 693 }
694 694 // TODO normalize
695 695 let filename = if directory.is_empty() {
696 696 filename.to_owned()
697 697 } else {
698 698 directory.join(&filename)
699 699 };
700 700
701 701 if !old_results.contains_key(filename.deref()) {
702 702 match self.handle_traversed_entry(
703 703 scope,
704 704 files_sender,
705 705 old_results,
706 706 filename,
707 707 dir_entry,
708 708 traversed_sender.clone(),
709 709 ) {
710 710 Err(e) => {
711 711 files_sender
712 712 .send((
713 713 directory.to_owned(),
714 714 dispatch_os_error(&e),
715 715 ))
716 716 .expect("receiver should outlive sender");
717 717 }
718 718 Ok(_) => {}
719 719 }
720 720 }
721 721 }
722 722 })
723 723 }
724 724
725 725 /// Add the files in the dirstate to the results.
726 726 ///
727 727 /// This takes a mutable reference to the results to account for the
728 728 /// `extend` in timings
729 729 #[timed]
730 730 pub fn extend_from_dmap(&self, results: &mut Vec<DispatchedPath<'a>>) {
731 731 results.par_extend(
732 732 self.dmap
733 733 .par_iter()
734 734 .filter(|(path, _)| self.matcher.matches(path))
735 735 .map(move |(filename, entry)| {
736 736 let filename: &HgPath = filename;
737 737 let filename_as_path = match hg_path_to_path_buf(filename)
738 738 {
739 739 Ok(f) => f,
740 740 Err(_) => {
741 741 return (
742 742 Cow::Borrowed(filename),
743 743 INVALID_PATH_DISPATCH,
744 744 )
745 745 }
746 746 };
747 747 let meta = self
748 748 .root_dir
749 749 .join(filename_as_path)
750 750 .symlink_metadata();
751 751 match meta {
752 752 Ok(m)
753 753 if !(m.file_type().is_file()
754 754 || m.file_type().is_symlink()) =>
755 755 {
756 756 (
757 757 Cow::Borrowed(filename),
758 758 dispatch_missing(entry.state),
759 759 )
760 760 }
761 761 Ok(m) => (
762 762 Cow::Borrowed(filename),
763 763 dispatch_found(
764 764 filename,
765 765 *entry,
766 766 HgMetadata::from_metadata(m),
767 767 &self.dmap.copy_map,
768 768 self.options,
769 769 ),
770 770 ),
771 771 Err(e)
772 772 if e.kind() == ErrorKind::NotFound
773 773 || e.raw_os_error() == Some(20) =>
774 774 {
775 775 // Rust does not yet have an `ErrorKind` for
776 776 // `NotADirectory` (errno 20)
777 777 // It happens if the dirstate contains `foo/bar`
778 778 // and foo is not a
779 779 // directory
780 780 (
781 781 Cow::Borrowed(filename),
782 782 dispatch_missing(entry.state),
783 783 )
784 784 }
785 785 Err(e) => {
786 786 (Cow::Borrowed(filename), dispatch_os_error(&e))
787 787 }
788 788 }
789 789 }),
790 790 );
791 791 }
792 792
793 793 /// Checks all files that are in the dirstate but were not found during the
794 794 /// working directory traversal. This means that the rest must
795 795 /// be either ignored, under a symlink or under a new nested repo.
796 796 ///
797 797 /// This takes a mutable reference to the results to account for the
798 798 /// `extend` in timings
799 799 #[timed]
800 800 pub fn handle_unknowns(&self, results: &mut Vec<DispatchedPath<'a>>) {
801 801 let to_visit: Vec<(&HgPath, &DirstateEntry)> =
802 802 if results.is_empty() && self.matcher.matches_everything() {
803 803 self.dmap.iter().map(|(f, e)| (f.deref(), e)).collect()
804 804 } else {
805 805 // Only convert to a hashmap if needed.
806 806 let old_results: FastHashMap<_, _> =
807 807 results.iter().cloned().collect();
808 808 self.dmap
809 809 .iter()
810 810 .filter_map(move |(f, e)| {
811 811 if !old_results.contains_key(f.deref())
812 812 && self.matcher.matches(f)
813 813 {
814 814 Some((f.deref(), e))
815 815 } else {
816 816 None
817 817 }
818 818 })
819 819 .collect()
820 820 };
821 821
822 822 let path_auditor = PathAuditor::new(&self.root_dir);
823 823
824 824 let new_results = to_visit.into_par_iter().filter_map(
825 825 |(filename, entry)| -> Option<_> {
826 826 // Report ignored items in the dmap as long as they are not
827 827 // under a symlink directory.
828 828 if path_auditor.check(filename) {
829 829 // TODO normalize for case-insensitive filesystems
830 830 let buf = match hg_path_to_path_buf(filename) {
831 831 Ok(x) => x,
832 832 Err(_) => {
833 833 return Some((
834 834 Cow::Owned(filename.to_owned()),
835 835 INVALID_PATH_DISPATCH,
836 836 ));
837 837 }
838 838 };
839 839 Some((
840 840 Cow::Owned(filename.to_owned()),
841 841 match self.root_dir.join(&buf).symlink_metadata() {
842 842 // File was just ignored, no links, and exists
843 843 Ok(meta) => {
844 844 let metadata = HgMetadata::from_metadata(meta);
845 845 dispatch_found(
846 846 filename,
847 847 *entry,
848 848 metadata,
849 849 &self.dmap.copy_map,
850 850 self.options,
851 851 )
852 852 }
853 853 // File doesn't exist
854 854 Err(_) => dispatch_missing(entry.state),
855 855 },
856 856 ))
857 857 } else {
858 858 // It's either missing or under a symlink directory which
859 859 // we, in this case, report as missing.
860 860 Some((
861 861 Cow::Owned(filename.to_owned()),
862 862 dispatch_missing(entry.state),
863 863 ))
864 864 }
865 865 },
866 866 );
867 867
868 868 results.par_extend(new_results);
869 869 }
870 870 }
871 871
872 872 #[timed]
873 873 pub fn build_response<'a>(
874 874 results: impl IntoIterator<Item = DispatchedPath<'a>>,
875 875 traversed: Vec<HgPathBuf>,
876 876 ) -> DirstateStatus<'a> {
877 877 let mut unsure = vec![];
878 878 let mut modified = vec![];
879 879 let mut added = vec![];
880 880 let mut removed = vec![];
881 881 let mut deleted = vec![];
882 882 let mut clean = vec![];
883 883 let mut ignored = vec![];
884 884 let mut unknown = vec![];
885 885 let mut bad = vec![];
886 886
887 887 for (filename, dispatch) in results.into_iter() {
888 888 match dispatch {
889 889 Dispatch::Unknown => unknown.push(filename),
890 890 Dispatch::Unsure => unsure.push(filename),
891 891 Dispatch::Modified => modified.push(filename),
892 892 Dispatch::Added => added.push(filename),
893 893 Dispatch::Removed => removed.push(filename),
894 894 Dispatch::Deleted => deleted.push(filename),
895 895 Dispatch::Clean => clean.push(filename),
896 896 Dispatch::Ignored => ignored.push(filename),
897 897 Dispatch::None => {}
898 898 Dispatch::Bad(reason) => bad.push((filename, reason)),
899 899 Dispatch::Directory { .. } => {}
900 900 }
901 901 }
902 902
903 903 DirstateStatus {
904 904 modified,
905 905 added,
906 906 removed,
907 907 deleted,
908 908 clean,
909 909 ignored,
910 910 unknown,
911 911 bad,
912 912 unsure,
913 913 traversed,
914 914 }
915 915 }
916 916
917 917 /// Get the status of files in the working directory.
918 918 ///
919 919 /// This is the current entry-point for `hg-core` and is realistically unusable
920 920 /// outside of a Python context because its arguments need to provide a lot of
921 921 /// information that will not be necessary in the future.
922 922 #[timed]
923 923 pub fn status<'a>(
924 924 dmap: &'a DirstateMap,
925 925 matcher: &'a (dyn Matcher + Sync),
926 926 root_dir: PathBuf,
927 927 ignore_files: Vec<PathBuf>,
928 928 options: StatusOptions,
929 929 ) -> StatusResult<(DirstateStatus<'a>, Vec<PatternFileWarning>)> {
930 930 let (status, warnings) =
931 931 Status::new(dmap, matcher, root_dir, ignore_files, options)?;
932 932
933 933 Ok((status.run()?, warnings))
934 934 }
@@ -1,652 +1,656 b''
1 1 use bytes_cast::BytesCast;
2 2 use std::path::PathBuf;
3 3 use std::{collections::BTreeMap, convert::TryInto};
4 4
5 5 use super::path_with_basename::WithBasename;
6 6 use crate::dirstate::parsers::clear_ambiguous_mtime;
7 7 use crate::dirstate::parsers::pack_entry;
8 8 use crate::dirstate::parsers::packed_entry_size;
9 9 use crate::dirstate::parsers::parse_dirstate_entries;
10 10 use crate::dirstate::parsers::parse_dirstate_parents;
11 11 use crate::dirstate::parsers::Timestamp;
12 12 use crate::matchers::Matcher;
13 13 use crate::revlog::node::NULL_NODE;
14 14 use crate::utils::hg_path::{HgPath, HgPathBuf};
15 15 use crate::CopyMapIter;
16 16 use crate::DirstateEntry;
17 17 use crate::DirstateError;
18 18 use crate::DirstateMapError;
19 19 use crate::DirstateParents;
20 20 use crate::DirstateStatus;
21 21 use crate::EntryState;
22 22 use crate::PatternFileWarning;
23 23 use crate::StateMapIter;
24 24 use crate::StatusError;
25 25 use crate::StatusOptions;
26 26
27 27 pub struct DirstateMap {
28 28 parents: Option<DirstateParents>,
29 29 dirty_parents: bool,
30 root: ChildNodes,
30 pub(super) root: ChildNodes,
31 31
32 32 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
33 33 nodes_with_entry_count: usize,
34 34
35 35 /// Number of nodes anywhere in the tree that have
36 36 /// `.copy_source.is_some()`.
37 37 nodes_with_copy_source_count: usize,
38 38 }
39 39
40 40 /// Using a plain `HgPathBuf` of the full path from the repository root as a
41 41 /// map key would also work: all paths in a given map have the same parent
42 42 /// path, so comparing full paths gives the same result as comparing base
43 43 /// names. However `BTreeMap` would waste time always re-comparing the same
44 44 /// string prefix.
45 type ChildNodes = BTreeMap<WithBasename<HgPathBuf>, Node>;
45 pub(super) type ChildNodes = BTreeMap<WithBasename<HgPathBuf>, Node>;
46 46
47 47 /// Represents a file or a directory
48 48 #[derive(Default)]
49 struct Node {
49 pub(super) struct Node {
50 50 /// `None` for directories
51 entry: Option<DirstateEntry>,
51 pub(super) entry: Option<DirstateEntry>,
52 52
53 copy_source: Option<HgPathBuf>,
53 pub(super) copy_source: Option<HgPathBuf>,
54 54
55 children: ChildNodes,
55 pub(super) children: ChildNodes,
56 56
57 57 /// How many (non-inclusive) descendants of this node are tracked files
58 58 tracked_descendants_count: usize,
59 59 }
60 60
61 61 impl Node {
62 62 /// Whether this node has a `DirstateEntry` with `.state.is_tracked()`
63 63 fn is_tracked_file(&self) -> bool {
64 64 if let Some(entry) = &self.entry {
65 65 entry.state.is_tracked()
66 66 } else {
67 67 false
68 68 }
69 69 }
70
71 pub(super) fn state(&self) -> Option<EntryState> {
72 self.entry.as_ref().map(|entry| entry.state)
73 }
70 74 }
71 75
72 76 /// `(full_path, entry, copy_source)`
73 77 type NodeDataMut<'a> = (
74 78 &'a WithBasename<HgPathBuf>,
75 79 &'a mut Option<DirstateEntry>,
76 80 &'a mut Option<HgPathBuf>,
77 81 );
78 82
79 83 impl DirstateMap {
80 84 pub fn new() -> Self {
81 85 Self {
82 86 parents: None,
83 87 dirty_parents: false,
84 88 root: ChildNodes::new(),
85 89 nodes_with_entry_count: 0,
86 90 nodes_with_copy_source_count: 0,
87 91 }
88 92 }
89 93
90 94 fn get_node(&self, path: &HgPath) -> Option<&Node> {
91 95 let mut children = &self.root;
92 96 let mut components = path.components();
93 97 let mut component =
94 98 components.next().expect("expected at least one components");
95 99 loop {
96 100 let child = children.get(component)?;
97 101 if let Some(next_component) = components.next() {
98 102 component = next_component;
99 103 children = &child.children;
100 104 } else {
101 105 return Some(child);
102 106 }
103 107 }
104 108 }
105 109
106 110 /// Returns a mutable reference to the node at `path` if it exists
107 111 ///
108 112 /// This takes `root` instead of `&mut self` so that callers can mutate
109 113 /// other fields while the returned borrow is still valid
110 114 fn get_node_mut<'tree>(
111 115 root: &'tree mut ChildNodes,
112 116 path: &HgPath,
113 117 ) -> Option<&'tree mut Node> {
114 118 Self::each_and_get(root, path, |_| {})
115 119 }
116 120
117 121 /// Call `each` for each ancestor node of the one at `path` (not including
118 122 /// that node itself), starting from nearest the root.
119 123 ///
120 124 /// Panics (possibly after some calls to `each`) if there is no node at
121 125 /// `path`.
122 126 fn for_each_ancestor_node<'tree>(
123 127 &mut self,
124 128 path: &HgPath,
125 129 each: impl FnMut(&mut Node),
126 130 ) {
127 131 let parent = path.parent();
128 132 if !parent.is_empty() {
129 133 Self::each_and_get(&mut self.root, parent, each)
130 134 .expect("missing dirstate node");
131 135 }
132 136 }
133 137
134 138 /// Common implementation detail of `get_node_mut` and
135 139 /// `for_each_ancestor_node`
136 140 fn each_and_get<'tree>(
137 141 root: &'tree mut ChildNodes,
138 142 path: &HgPath,
139 143 mut each: impl FnMut(&mut Node),
140 144 ) -> Option<&'tree mut Node> {
141 145 let mut children = root;
142 146 let mut components = path.components();
143 147 let mut component =
144 148 components.next().expect("expected at least one components");
145 149 loop {
146 150 let child = children.get_mut(component)?;
147 151 each(child);
148 152 if let Some(next_component) = components.next() {
149 153 component = next_component;
150 154 children = &mut child.children;
151 155 } else {
152 156 return Some(child);
153 157 }
154 158 }
155 159 }
156 160
157 161 fn get_or_insert_node<'tree>(
158 162 root: &'tree mut ChildNodes,
159 163 path: &HgPath,
160 164 ) -> &'tree mut Node {
161 165 let mut child_nodes = root;
162 166 let mut inclusive_ancestor_paths =
163 167 WithBasename::inclusive_ancestors_of(path);
164 168 let mut ancestor_path = inclusive_ancestor_paths
165 169 .next()
166 170 .expect("expected at least one inclusive ancestor");
167 171 loop {
168 172 // TODO: can we avoid double lookup in all cases without allocating
169 173 // an owned key in cases where the map already contains that key?
170 174 let child_node =
171 175 if child_nodes.contains_key(ancestor_path.base_name()) {
172 176 child_nodes.get_mut(ancestor_path.base_name()).unwrap()
173 177 } else {
174 178 // This is always a vacant entry, using `.entry()` lets us
175 179 // return a `&mut Node` of the newly-inserted node without
176 180 // yet another lookup. `BTreeMap::insert` doesn’t do this.
177 181 child_nodes.entry(ancestor_path.to_owned()).or_default()
178 182 };
179 183 if let Some(next) = inclusive_ancestor_paths.next() {
180 184 ancestor_path = next;
181 185 child_nodes = &mut child_node.children;
182 186 } else {
183 187 return child_node;
184 188 }
185 189 }
186 190 }
187 191
188 192 /// The meaning of `new_copy_source` is:
189 193 ///
190 194 /// * `Some(Some(x))`: set `Node::copy_source` to `Some(x)`
191 195 /// * `Some(None)`: set `Node::copy_source` to `None`
192 196 /// * `None`: leave `Node::copy_source` unchanged
193 197 fn add_file_node(
194 198 &mut self,
195 199 path: &HgPath,
196 200 new_entry: DirstateEntry,
197 201 new_copy_source: Option<Option<HgPathBuf>>,
198 202 ) {
199 203 let node = Self::get_or_insert_node(&mut self.root, path);
200 204 if node.entry.is_none() {
201 205 self.nodes_with_entry_count += 1
202 206 }
203 207 if let Some(source) = &new_copy_source {
204 208 if node.copy_source.is_none() && source.is_some() {
205 209 self.nodes_with_copy_source_count += 1
206 210 }
207 211 if node.copy_source.is_some() && source.is_none() {
208 212 self.nodes_with_copy_source_count -= 1
209 213 }
210 214 }
211 215 let tracked_count_increment =
212 216 match (node.is_tracked_file(), new_entry.state.is_tracked()) {
213 217 (false, true) => 1,
214 218 (true, false) => -1,
215 219 _ => 0,
216 220 };
217 221
218 222 node.entry = Some(new_entry);
219 223 if let Some(source) = new_copy_source {
220 224 node.copy_source = source
221 225 }
222 226 // Borrow of `self.root` through `node` ends here
223 227
224 228 match tracked_count_increment {
225 229 1 => self.for_each_ancestor_node(path, |node| {
226 230 node.tracked_descendants_count += 1
227 231 }),
228 232 // We can’t use `+= -1` because the counter is unsigned
229 233 -1 => self.for_each_ancestor_node(path, |node| {
230 234 node.tracked_descendants_count -= 1
231 235 }),
232 236 _ => {}
233 237 }
234 238 }
235 239
236 240 fn iter_nodes<'a>(
237 241 &'a self,
238 242 ) -> impl Iterator<Item = (&'a WithBasename<HgPathBuf>, &'a Node)> + 'a
239 243 {
240 244 // Depth first tree traversal.
241 245 //
242 246 // If we could afford internal iteration and recursion,
243 247 // this would look like:
244 248 //
245 249 // ```
246 250 // fn traverse_children(
247 251 // children: &ChildNodes,
248 252 // each: &mut impl FnMut(&Node),
249 253 // ) {
250 254 // for child in children.values() {
251 255 // traverse_children(&child.children, each);
252 256 // each(child);
253 257 // }
254 258 // }
255 259 // ```
256 260 //
257 261 // However we want an external iterator and therefore can’t use the
258 262 // call stack. Use an explicit stack instead:
259 263 let mut stack = Vec::new();
260 264 let mut iter = self.root.iter();
261 265 std::iter::from_fn(move || {
262 266 while let Some((key, child_node)) = iter.next() {
263 267 // Pseudo-recursion
264 268 let new_iter = child_node.children.iter();
265 269 let old_iter = std::mem::replace(&mut iter, new_iter);
266 270 stack.push((key, child_node, old_iter));
267 271 }
268 272 // Found the end of a `children.iter()` iterator.
269 273 if let Some((key, child_node, next_iter)) = stack.pop() {
270 274 // "Return" from pseudo-recursion by restoring state from the
271 275 // explicit stack
272 276 iter = next_iter;
273 277
274 278 Some((key, child_node))
275 279 } else {
276 280 // Reached the bottom of the stack, we’re done
277 281 None
278 282 }
279 283 })
280 284 }
281 285
282 286 /// Mutable iterator for the `(entry, copy source)` of each node.
283 287 ///
284 288 /// It would not be safe to yield mutable references to nodes themeselves
285 289 /// with `-> impl Iterator<Item = &mut Node>` since child nodes are
286 290 /// reachable from their ancestor nodes, potentially creating multiple
287 291 /// `&mut` references to a given node.
288 292 fn iter_node_data_mut<'a>(
289 293 &'a mut self,
290 294 ) -> impl Iterator<Item = NodeDataMut<'a>> + 'a {
291 295 // Explict stack for pseudo-recursion, see `iter_nodes` above.
292 296 let mut stack = Vec::new();
293 297 let mut iter = self.root.iter_mut();
294 298 std::iter::from_fn(move || {
295 299 while let Some((key, child_node)) = iter.next() {
296 300 // Pseudo-recursion
297 301 let data =
298 302 (key, &mut child_node.entry, &mut child_node.copy_source);
299 303 let new_iter = child_node.children.iter_mut();
300 304 let old_iter = std::mem::replace(&mut iter, new_iter);
301 305 stack.push((data, old_iter));
302 306 }
303 307 // Found the end of a `children.values_mut()` iterator.
304 308 if let Some((data, next_iter)) = stack.pop() {
305 309 // "Return" from pseudo-recursion by restoring state from the
306 310 // explicit stack
307 311 iter = next_iter;
308 312
309 313 Some(data)
310 314 } else {
311 315 // Reached the bottom of the stack, we’re done
312 316 None
313 317 }
314 318 })
315 319 }
316 320 }
317 321
318 322 impl super::dispatch::DirstateMapMethods for DirstateMap {
319 323 fn clear(&mut self) {
320 324 self.set_parents(&DirstateParents {
321 325 p1: NULL_NODE,
322 326 p2: NULL_NODE,
323 327 });
324 328 self.root.clear();
325 329 self.nodes_with_entry_count = 0;
326 330 self.nodes_with_copy_source_count = 0;
327 331 }
328 332
329 333 fn add_file(
330 334 &mut self,
331 335 filename: &HgPath,
332 336 _old_state: EntryState,
333 337 entry: DirstateEntry,
334 338 ) -> Result<(), DirstateMapError> {
335 339 self.add_file_node(filename, entry, None);
336 340 Ok(())
337 341 }
338 342
339 343 fn remove_file(
340 344 &mut self,
341 345 filename: &HgPath,
342 346 _old_state: EntryState,
343 347 size: i32,
344 348 ) -> Result<(), DirstateMapError> {
345 349 let entry = DirstateEntry {
346 350 state: EntryState::Removed,
347 351 mode: 0,
348 352 size,
349 353 mtime: 0,
350 354 };
351 355 self.add_file_node(filename, entry, None);
352 356 Ok(())
353 357 }
354 358
355 359 fn drop_file(
356 360 &mut self,
357 361 filename: &HgPath,
358 362 _old_state: EntryState,
359 363 ) -> Result<bool, DirstateMapError> {
360 364 if let Some(node) = Self::get_node_mut(&mut self.root, filename) {
361 365 let was_tracked = node.is_tracked_file();
362 366 let had_entry = node.entry.is_some();
363 367 let had_copy_source = node.copy_source.is_some();
364 368
365 369 // TODO: this leaves in the tree a "non-file" node. Should we
366 370 // remove the node instead, together with ancestor nodes for
367 371 // directories that become empty?
368 372 node.entry = None;
369 373 node.copy_source = None;
370 374
371 375 if had_entry {
372 376 self.nodes_with_entry_count -= 1
373 377 }
374 378 if had_copy_source {
375 379 self.nodes_with_copy_source_count -= 1
376 380 }
377 381 if was_tracked {
378 382 self.for_each_ancestor_node(filename, |node| {
379 383 node.tracked_descendants_count -= 1
380 384 })
381 385 }
382 386 Ok(had_entry)
383 387 } else {
384 388 Ok(false)
385 389 }
386 390 }
387 391
388 392 fn clear_ambiguous_times(&mut self, filenames: Vec<HgPathBuf>, now: i32) {
389 393 for filename in filenames {
390 394 if let Some(node) = Self::get_node_mut(&mut self.root, &filename) {
391 395 if let Some(entry) = node.entry.as_mut() {
392 396 clear_ambiguous_mtime(entry, now);
393 397 }
394 398 }
395 399 }
396 400 }
397 401
398 402 fn non_normal_entries_contains(&mut self, key: &HgPath) -> bool {
399 403 self.get_node(key)
400 404 .and_then(|node| node.entry.as_ref())
401 405 .map_or(false, DirstateEntry::is_non_normal)
402 406 }
403 407
404 408 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
405 409 // Do nothing, this `DirstateMap` does not have a separate "non normal
406 410 // entries" set that need to be kept up to date
407 411 }
408 412
409 413 fn non_normal_or_other_parent_paths(
410 414 &mut self,
411 415 ) -> Box<dyn Iterator<Item = &HgPathBuf> + '_> {
412 416 Box::new(self.iter_nodes().filter_map(|(path, node)| {
413 417 node.entry
414 418 .as_ref()
415 419 .filter(|entry| {
416 420 entry.is_non_normal() || entry.is_from_other_parent()
417 421 })
418 422 .map(|_| path.full_path())
419 423 }))
420 424 }
421 425
422 426 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
423 427 // Do nothing, this `DirstateMap` does not have a separate "non normal
424 428 // entries" and "from other parent" sets that need to be recomputed
425 429 }
426 430
427 431 fn iter_non_normal_paths(
428 432 &mut self,
429 433 ) -> Box<dyn Iterator<Item = &HgPathBuf> + Send + '_> {
430 434 self.iter_non_normal_paths_panic()
431 435 }
432 436
433 437 fn iter_non_normal_paths_panic(
434 438 &self,
435 439 ) -> Box<dyn Iterator<Item = &HgPathBuf> + Send + '_> {
436 440 Box::new(self.iter_nodes().filter_map(|(path, node)| {
437 441 node.entry
438 442 .as_ref()
439 443 .filter(|entry| entry.is_non_normal())
440 444 .map(|_| path.full_path())
441 445 }))
442 446 }
443 447
444 448 fn iter_other_parent_paths(
445 449 &mut self,
446 450 ) -> Box<dyn Iterator<Item = &HgPathBuf> + Send + '_> {
447 451 Box::new(self.iter_nodes().filter_map(|(path, node)| {
448 452 node.entry
449 453 .as_ref()
450 454 .filter(|entry| entry.is_from_other_parent())
451 455 .map(|_| path.full_path())
452 456 }))
453 457 }
454 458
455 459 fn has_tracked_dir(
456 460 &mut self,
457 461 directory: &HgPath,
458 462 ) -> Result<bool, DirstateMapError> {
459 463 if let Some(node) = self.get_node(directory) {
460 464 // A node without a `DirstateEntry` was created to hold child
461 465 // nodes, and is therefore a directory.
462 466 Ok(node.entry.is_none() && node.tracked_descendants_count > 0)
463 467 } else {
464 468 Ok(false)
465 469 }
466 470 }
467 471
468 472 fn has_dir(
469 473 &mut self,
470 474 directory: &HgPath,
471 475 ) -> Result<bool, DirstateMapError> {
472 476 if let Some(node) = self.get_node(directory) {
473 477 // A node without a `DirstateEntry` was created to hold child
474 478 // nodes, and is therefore a directory.
475 479 Ok(node.entry.is_none())
476 480 } else {
477 481 Ok(false)
478 482 }
479 483 }
480 484
481 485 fn parents(
482 486 &mut self,
483 487 file_contents: &[u8],
484 488 ) -> Result<&DirstateParents, DirstateError> {
485 489 if self.parents.is_none() {
486 490 let parents = if !file_contents.is_empty() {
487 491 parse_dirstate_parents(file_contents)?.clone()
488 492 } else {
489 493 DirstateParents {
490 494 p1: NULL_NODE,
491 495 p2: NULL_NODE,
492 496 }
493 497 };
494 498 self.parents = Some(parents);
495 499 }
496 500 Ok(self.parents.as_ref().unwrap())
497 501 }
498 502
499 503 fn set_parents(&mut self, parents: &DirstateParents) {
500 504 self.parents = Some(parents.clone());
501 505 self.dirty_parents = true;
502 506 }
503 507
504 508 fn read<'a>(
505 509 &mut self,
506 510 file_contents: &'a [u8],
507 511 ) -> Result<Option<&'a DirstateParents>, DirstateError> {
508 512 if file_contents.is_empty() {
509 513 return Ok(None);
510 514 }
511 515
512 516 let parents = parse_dirstate_entries(
513 517 file_contents,
514 518 |path, entry, copy_source| {
515 519 self.add_file_node(
516 520 path,
517 521 *entry,
518 522 Some(copy_source.map(HgPath::to_owned)),
519 523 )
520 524 },
521 525 )?;
522 526
523 527 if !self.dirty_parents {
524 528 self.set_parents(parents);
525 529 }
526 530
527 531 Ok(Some(parents))
528 532 }
529 533
530 534 fn pack(
531 535 &mut self,
532 536 parents: DirstateParents,
533 537 now: Timestamp,
534 538 ) -> Result<Vec<u8>, DirstateError> {
535 539 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
536 540 // reallocations
537 541 let mut size = parents.as_bytes().len();
538 542 for (path, node) in self.iter_nodes() {
539 543 if node.entry.is_some() {
540 544 size += packed_entry_size(
541 545 path.full_path(),
542 546 node.copy_source.as_ref(),
543 547 )
544 548 }
545 549 }
546 550
547 551 let mut packed = Vec::with_capacity(size);
548 552 packed.extend(parents.as_bytes());
549 553
550 554 let now: i32 = now.0.try_into().expect("time overflow");
551 555 for (path, opt_entry, copy_source) in self.iter_node_data_mut() {
552 556 if let Some(entry) = opt_entry {
553 557 clear_ambiguous_mtime(entry, now);
554 558 pack_entry(
555 559 path.full_path(),
556 560 entry,
557 561 copy_source.as_ref(),
558 562 &mut packed,
559 563 );
560 564 }
561 565 }
562 566 self.dirty_parents = false;
563 567 Ok(packed)
564 568 }
565 569
566 570 fn set_all_dirs(&mut self) -> Result<(), DirstateMapError> {
567 571 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
568 572 // needs to be recomputed
569 573 Ok(())
570 574 }
571 575
572 576 fn set_dirs(&mut self) -> Result<(), DirstateMapError> {
573 577 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
574 578 // to be recomputed
575 579 Ok(())
576 580 }
577 581
578 582 fn status<'a>(
579 583 &'a mut self,
580 584 matcher: &'a (dyn Matcher + Sync),
581 585 root_dir: PathBuf,
582 586 ignore_files: Vec<PathBuf>,
583 587 options: StatusOptions,
584 588 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
585 589 {
586 590 super::status::status(self, matcher, root_dir, ignore_files, options)
587 591 }
588 592
589 593 fn copy_map_len(&self) -> usize {
590 594 self.nodes_with_copy_source_count
591 595 }
592 596
593 597 fn copy_map_iter(&self) -> CopyMapIter<'_> {
594 598 Box::new(self.iter_nodes().filter_map(|(path, node)| {
595 599 node.copy_source
596 600 .as_ref()
597 601 .map(|copy_source| (path.full_path(), copy_source))
598 602 }))
599 603 }
600 604
601 605 fn copy_map_contains_key(&self, key: &HgPath) -> bool {
602 606 if let Some(node) = self.get_node(key) {
603 607 node.copy_source.is_some()
604 608 } else {
605 609 false
606 610 }
607 611 }
608 612
609 613 fn copy_map_get(&self, key: &HgPath) -> Option<&HgPathBuf> {
610 614 self.get_node(key)?.copy_source.as_ref()
611 615 }
612 616
613 617 fn copy_map_remove(&mut self, key: &HgPath) -> Option<HgPathBuf> {
614 618 let count = &mut self.nodes_with_copy_source_count;
615 619 Self::get_node_mut(&mut self.root, key).and_then(|node| {
616 620 if node.copy_source.is_some() {
617 621 *count -= 1
618 622 }
619 623 node.copy_source.take()
620 624 })
621 625 }
622 626
623 627 fn copy_map_insert(
624 628 &mut self,
625 629 key: HgPathBuf,
626 630 value: HgPathBuf,
627 631 ) -> Option<HgPathBuf> {
628 632 let node = Self::get_or_insert_node(&mut self.root, &key);
629 633 if node.copy_source.is_none() {
630 634 self.nodes_with_copy_source_count += 1
631 635 }
632 636 node.copy_source.replace(value)
633 637 }
634 638
635 639 fn len(&self) -> usize {
636 640 self.nodes_with_entry_count
637 641 }
638 642
639 643 fn contains_key(&self, key: &HgPath) -> bool {
640 644 self.get(key).is_some()
641 645 }
642 646
643 647 fn get(&self, key: &HgPath) -> Option<&DirstateEntry> {
644 648 self.get_node(key)?.entry.as_ref()
645 649 }
646 650
647 651 fn iter(&self) -> StateMapIter<'_> {
648 652 Box::new(self.iter_nodes().filter_map(|(path, node)| {
649 653 node.entry.as_ref().map(|entry| (path.full_path(), entry))
650 654 }))
651 655 }
652 656 }
@@ -1,17 +1,379 b''
1 use crate::dirstate::status::IgnoreFnType;
2 use crate::dirstate_tree::dirstate_map::ChildNodes;
1 3 use crate::dirstate_tree::dirstate_map::DirstateMap;
4 use crate::dirstate_tree::dirstate_map::Node;
5 use crate::matchers::get_ignore_function;
2 6 use crate::matchers::Matcher;
7 use crate::utils::files::get_bytes_from_os_string;
8 use crate::utils::hg_path::HgPath;
3 9 use crate::DirstateStatus;
10 use crate::EntryState;
11 use crate::HgPathBuf;
4 12 use crate::PatternFileWarning;
5 13 use crate::StatusError;
6 14 use crate::StatusOptions;
15 use std::borrow::Cow;
16 use std::io;
17 use std::path::Path;
7 18 use std::path::PathBuf;
8 19
9 pub fn status<'a>(
10 _dmap: &'a mut DirstateMap,
11 _matcher: &'a (dyn Matcher + Sync),
12 _root_dir: PathBuf,
13 _ignore_files: Vec<PathBuf>,
14 _options: StatusOptions,
15 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError> {
16 todo!()
20 /// Returns the status of the working directory compared to its parent
21 /// changeset.
22 ///
23 /// This algorithm is based on traversing the filesystem tree (`fs` in function
24 /// and variable names) and dirstate tree at the same time. The core of this
25 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
26 /// and its use of `itertools::merge_join_by`. When reaching a path that only
27 /// exists in one of the two trees, depending on information requested by
28 /// `options` we may need to traverse the remaining subtree.
29 pub fn status<'tree>(
30 dmap: &'tree mut DirstateMap,
31 matcher: &(dyn Matcher + Sync),
32 root_dir: PathBuf,
33 ignore_files: Vec<PathBuf>,
34 options: StatusOptions,
35 ) -> Result<(DirstateStatus<'tree>, Vec<PatternFileWarning>), StatusError> {
36 let (ignore_fn, warnings): (IgnoreFnType, _) =
37 if options.list_ignored || options.list_unknown {
38 get_ignore_function(ignore_files, &root_dir)?
39 } else {
40 (Box::new(|&_| true), vec![])
41 };
42
43 let mut common = StatusCommon {
44 options,
45 matcher,
46 ignore_fn,
47 outcome: DirstateStatus::default(),
48 };
49 let is_at_repo_root = true;
50 let hg_path = HgPath::new("");
51 let has_ignored_ancestor = false;
52 common.traverse_fs_directory_and_dirstate(
53 has_ignored_ancestor,
54 &mut dmap.root,
55 hg_path,
56 &root_dir,
57 is_at_repo_root,
58 );
59 Ok((common.outcome, warnings))
60 }
61
62 /// Bag of random things needed by various parts of the algorithm. Reduces the
63 /// number of parameters passed to functions.
64 struct StatusCommon<'tree, 'a> {
65 options: StatusOptions,
66 matcher: &'a (dyn Matcher + Sync),
67 ignore_fn: IgnoreFnType<'a>,
68 outcome: DirstateStatus<'tree>,
17 69 }
70
71 impl<'tree, 'a> StatusCommon<'tree, 'a> {
72 fn traverse_fs_directory_and_dirstate(
73 &mut self,
74 has_ignored_ancestor: bool,
75 dirstate_nodes: &'tree mut ChildNodes,
76 directory_hg_path: &HgPath,
77 fs_path: &Path,
78 is_at_repo_root: bool,
79 ) {
80 // TODO: handle I/O errors
81 let mut fs_entries =
82 DirEntry::read_dir(fs_path, is_at_repo_root).unwrap();
83
84 // `merge_join_by` requires both its input iterators to be sorted:
85
86 // * `BTreeMap` iterates according to keys’ ordering by definition
87
88 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
89 // https://github.com/rust-lang/rust/issues/34162
90 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
91
92 for pair in itertools::merge_join_by(
93 dirstate_nodes,
94 &fs_entries,
95 |(full_path, _node), fs_entry| {
96 full_path.base_name().cmp(&fs_entry.base_name)
97 },
98 ) {
99 use itertools::EitherOrBoth::*;
100 match pair {
101 Both((hg_path, dirstate_node), fs_entry) => {
102 self.traverse_fs_and_dirstate(
103 fs_entry,
104 hg_path.full_path(),
105 dirstate_node,
106 has_ignored_ancestor,
107 );
108 }
109 Left((hg_path, dirstate_node)) => self.traverse_dirstate_only(
110 hg_path.full_path(),
111 dirstate_node,
112 ),
113 Right(fs_entry) => self.traverse_fs_only(
114 has_ignored_ancestor,
115 directory_hg_path,
116 fs_entry,
117 ),
118 }
119 }
120 }
121
122 fn traverse_fs_and_dirstate(
123 &mut self,
124 fs_entry: &DirEntry,
125 hg_path: &'tree HgPath,
126 dirstate_node: &'tree mut Node,
127 has_ignored_ancestor: bool,
128 ) {
129 if fs_entry.metadata.is_dir() {
130 if self.options.collect_traversed_dirs {
131 self.outcome.traversed.push(hg_path.into())
132 }
133 // If we previously had a file here, it was removed (with
134 // `hg rm` or similar) or deleted before it could be
135 // replaced by a directory.
136 self.mark_removed_or_deleted_if_file(
137 hg_path,
138 dirstate_node.state(),
139 );
140 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
141 let is_at_repo_root = false;
142 self.traverse_fs_directory_and_dirstate(
143 is_ignored,
144 &mut dirstate_node.children,
145 hg_path,
146 &fs_entry.full_path,
147 is_at_repo_root,
148 );
149 } else {
150 if self.matcher.matches(hg_path) {
151 let full_path = Cow::from(hg_path);
152 if let Some(entry) = &dirstate_node.entry {
153 match entry.state {
154 EntryState::Added => {
155 self.outcome.added.push(full_path)
156 }
157 EntryState::Removed => {
158 self.outcome.removed.push(full_path)
159 }
160 EntryState::Merged => {
161 self.outcome.modified.push(full_path)
162 }
163 EntryState::Normal => {
164 self.handle_normal_file(
165 full_path,
166 dirstate_node,
167 entry,
168 fs_entry,
169 );
170 }
171 // This variant is not used in DirstateMap
172 // nodes
173 EntryState::Unknown => unreachable!(),
174 }
175 } else {
176 // `node.entry.is_none()` indicates a "directory"
177 // node, but the filesystem has a file
178 self.mark_unknown_or_ignored(
179 has_ignored_ancestor,
180 full_path,
181 )
182 }
183 }
184
185 for (child_hg_path, child_node) in &mut dirstate_node.children {
186 self.traverse_dirstate_only(
187 child_hg_path.full_path(),
188 child_node,
189 )
190 }
191 }
192 }
193
194 /// A file with `EntryState::Normal` in the dirstate was found in the
195 /// filesystem
196 fn handle_normal_file(
197 &mut self,
198 full_path: Cow<'tree, HgPath>,
199 dirstate_node: &Node,
200 entry: &crate::DirstateEntry,
201 fs_entry: &DirEntry,
202 ) {
203 // Keep the low 31 bits
204 fn truncate_u64(value: u64) -> i32 {
205 (value & 0x7FFF_FFFF) as i32
206 }
207 fn truncate_i64(value: i64) -> i32 {
208 (value & 0x7FFF_FFFF) as i32
209 }
210
211 let mode_changed = || {
212 self.options.check_exec && entry.mode_changed(&fs_entry.metadata)
213 };
214 let size_changed = entry.size != truncate_u64(fs_entry.metadata.len());
215 if entry.size >= 0
216 && size_changed
217 && fs_entry.metadata.file_type().is_symlink()
218 {
219 // issue6456: Size returned may be longer due to encryption
220 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
221 self.outcome.unsure.push(full_path)
222 } else if dirstate_node.copy_source.is_some()
223 || entry.is_from_other_parent()
224 || (entry.size >= 0 && (size_changed || mode_changed()))
225 {
226 self.outcome.modified.push(full_path)
227 } else {
228 let mtime = mtime_seconds(&fs_entry.metadata);
229 if truncate_i64(mtime) != entry.mtime
230 || mtime == self.options.last_normal_time
231 {
232 self.outcome.unsure.push(full_path)
233 } else if self.options.list_clean {
234 self.outcome.clean.push(full_path)
235 }
236 }
237 }
238
239 /// A node in the dirstate tree has no corresponding filesystem entry
240 fn traverse_dirstate_only(
241 &mut self,
242 hg_path: &'tree HgPath,
243 dirstate_node: &'tree mut Node,
244 ) {
245 self.mark_removed_or_deleted_if_file(hg_path, dirstate_node.state());
246 for (child_hg_path, child_node) in &mut dirstate_node.children {
247 self.traverse_dirstate_only(child_hg_path.full_path(), child_node)
248 }
249 }
250
251 /// A node in the dirstate tree has no corresponding *file* on the
252 /// filesystem
253 ///
254 /// Does nothing on a "directory" node
255 fn mark_removed_or_deleted_if_file(
256 &mut self,
257 hg_path: &'tree HgPath,
258 dirstate_node_state: Option<EntryState>,
259 ) {
260 if let Some(state) = dirstate_node_state {
261 if self.matcher.matches(hg_path) {
262 if let EntryState::Removed = state {
263 self.outcome.removed.push(hg_path.into())
264 } else {
265 self.outcome.deleted.push(hg_path.into())
266 }
267 }
268 }
269 }
270
271 /// Something in the filesystem has no corresponding dirstate node
272 fn traverse_fs_only(
273 &mut self,
274 has_ignored_ancestor: bool,
275 directory_hg_path: &HgPath,
276 fs_entry: &DirEntry,
277 ) {
278 let hg_path = directory_hg_path.join(&fs_entry.base_name);
279 if fs_entry.metadata.is_dir() {
280 let is_ignored =
281 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
282 let traverse_children = if is_ignored {
283 // Descendants of an ignored directory are all ignored
284 self.options.list_ignored
285 } else {
286 // Descendants of an unknown directory may be either unknown or
287 // ignored
288 self.options.list_unknown || self.options.list_ignored
289 };
290 if traverse_children {
291 let is_at_repo_root = false;
292 // TODO: handle I/O errors
293 let children_fs_entries =
294 DirEntry::read_dir(&fs_entry.full_path, is_at_repo_root)
295 .unwrap();
296 for child_fs_entry in children_fs_entries {
297 self.traverse_fs_only(
298 is_ignored,
299 &hg_path,
300 &child_fs_entry,
301 )
302 }
303 }
304 if self.options.collect_traversed_dirs {
305 self.outcome.traversed.push(hg_path.into())
306 }
307 } else if self.matcher.matches(&hg_path) {
308 self.mark_unknown_or_ignored(has_ignored_ancestor, hg_path.into())
309 }
310 }
311
312 fn mark_unknown_or_ignored(
313 &mut self,
314 has_ignored_ancestor: bool,
315 hg_path: Cow<'tree, HgPath>,
316 ) {
317 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
318 if is_ignored {
319 if self.options.list_ignored {
320 self.outcome.ignored.push(hg_path)
321 }
322 } else {
323 if self.options.list_unknown {
324 self.outcome.unknown.push(hg_path)
325 }
326 }
327 }
328 }
329
330 #[cfg(unix)] // TODO
331 fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
332 // Going through `Metadata::modified()` would be portable, but would take
333 // care to construct a `SystemTime` value with sub-second precision just
334 // for us to throw that away here.
335 use std::os::unix::fs::MetadataExt;
336 metadata.mtime()
337 }
338
339 struct DirEntry {
340 base_name: HgPathBuf,
341 full_path: PathBuf,
342 metadata: std::fs::Metadata,
343 }
344
345 impl DirEntry {
346 /// Returns **unsorted** entries in the given directory, with name and
347 /// metadata.
348 ///
349 /// If a `.hg` sub-directory is encountered:
350 ///
351 /// * At the repository root, ignore that sub-directory
352 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
353 /// list instead.
354 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
355 let mut results = Vec::new();
356 for entry in path.read_dir()? {
357 let entry = entry?;
358 let metadata = entry.metadata()?;
359 let name = get_bytes_from_os_string(entry.file_name());
360 // FIXME don't do this when cached
361 if name == b".hg" {
362 if is_at_repo_root {
363 // Skip the repo’s own .hg (might be a symlink)
364 continue;
365 } else if metadata.is_dir() {
366 // A .hg sub-directory at another location means a subrepo,
367 // skip it entirely.
368 return Ok(Vec::new());
369 }
370 }
371 results.push(DirEntry {
372 base_name: name.into(),
373 full_path: entry.path(),
374 metadata,
375 })
376 }
377 Ok(results)
378 }
379 }
@@ -1,463 +1,469 b''
1 1 // files.rs
2 2 //
3 3 // Copyright 2019
4 4 // Raphaël Gomès <rgomes@octobus.net>,
5 5 // Yuya Nishihara <yuya@tcha.org>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 //! Functions for fiddling with files.
11 11
12 12 use crate::utils::{
13 13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
14 14 path_auditor::PathAuditor,
15 15 replace_slice,
16 16 };
17 17 use lazy_static::lazy_static;
18 18 use same_file::is_same_file;
19 19 use std::borrow::{Cow, ToOwned};
20 use std::ffi::OsStr;
20 use std::ffi::{OsStr, OsString};
21 21 use std::fs::Metadata;
22 22 use std::iter::FusedIterator;
23 23 use std::ops::Deref;
24 24 use std::path::{Path, PathBuf};
25 25
26 26 pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr {
27 27 let os_str;
28 28 #[cfg(unix)]
29 29 {
30 30 use std::os::unix::ffi::OsStrExt;
31 31 os_str = std::ffi::OsStr::from_bytes(bytes);
32 32 }
33 33 // TODO Handle other platforms
34 34 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
35 35 // Perhaps, the return type would have to be Result<PathBuf>.
36 36 os_str
37 37 }
38 38
39 39 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
40 40 Path::new(get_os_str_from_bytes(bytes))
41 41 }
42 42
43 43 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
44 44 // that's why Vec<u8> is returned.
45 45 #[cfg(unix)]
46 46 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
47 47 get_bytes_from_os_str(path.as_ref())
48 48 }
49 49
50 50 #[cfg(unix)]
51 51 pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> {
52 52 use std::os::unix::ffi::OsStrExt;
53 53 str.as_ref().as_bytes().to_vec()
54 54 }
55 55
56 #[cfg(unix)]
57 pub fn get_bytes_from_os_string(str: OsString) -> Vec<u8> {
58 use std::os::unix::ffi::OsStringExt;
59 str.into_vec()
60 }
61
56 62 /// An iterator over repository path yielding itself and its ancestors.
57 63 #[derive(Copy, Clone, Debug)]
58 64 pub struct Ancestors<'a> {
59 65 next: Option<&'a HgPath>,
60 66 }
61 67
62 68 impl<'a> Iterator for Ancestors<'a> {
63 69 type Item = &'a HgPath;
64 70
65 71 fn next(&mut self) -> Option<Self::Item> {
66 72 let next = self.next;
67 73 self.next = match self.next {
68 74 Some(s) if s.is_empty() => None,
69 75 Some(s) => {
70 76 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
71 77 Some(HgPath::new(&s.as_bytes()[..p]))
72 78 }
73 79 None => None,
74 80 };
75 81 next
76 82 }
77 83 }
78 84
79 85 impl<'a> FusedIterator for Ancestors<'a> {}
80 86
81 87 /// An iterator over repository path yielding itself and its ancestors.
82 88 #[derive(Copy, Clone, Debug)]
83 89 pub(crate) struct AncestorsWithBase<'a> {
84 90 next: Option<(&'a HgPath, &'a HgPath)>,
85 91 }
86 92
87 93 impl<'a> Iterator for AncestorsWithBase<'a> {
88 94 type Item = (&'a HgPath, &'a HgPath);
89 95
90 96 fn next(&mut self) -> Option<Self::Item> {
91 97 let next = self.next;
92 98 self.next = match self.next {
93 99 Some((s, _)) if s.is_empty() => None,
94 100 Some((s, _)) => Some(s.split_filename()),
95 101 None => None,
96 102 };
97 103 next
98 104 }
99 105 }
100 106
101 107 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
102 108
103 109 /// Returns an iterator yielding ancestor directories of the given repository
104 110 /// path.
105 111 ///
106 112 /// The path is separated by '/', and must not start with '/'.
107 113 ///
108 114 /// The path itself isn't included unless it is b"" (meaning the root
109 115 /// directory.)
110 116 pub fn find_dirs(path: &HgPath) -> Ancestors {
111 117 let mut dirs = Ancestors { next: Some(path) };
112 118 if !path.is_empty() {
113 119 dirs.next(); // skip itself
114 120 }
115 121 dirs
116 122 }
117 123
118 124 /// Returns an iterator yielding ancestor directories of the given repository
119 125 /// path.
120 126 ///
121 127 /// The path is separated by '/', and must not start with '/'.
122 128 ///
123 129 /// The path itself isn't included unless it is b"" (meaning the root
124 130 /// directory.)
125 131 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
126 132 let mut dirs = AncestorsWithBase {
127 133 next: Some((path, HgPath::new(b""))),
128 134 };
129 135 if !path.is_empty() {
130 136 dirs.next(); // skip itself
131 137 }
132 138 dirs
133 139 }
134 140
135 141 /// TODO more than ASCII?
136 142 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
137 143 #[cfg(windows)] // NTFS compares via upper()
138 144 return path.to_ascii_uppercase();
139 145 #[cfg(unix)]
140 146 path.to_ascii_lowercase()
141 147 }
142 148
143 149 lazy_static! {
144 150 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
145 151 [
146 152 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
147 153 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
148 154 ]
149 155 .iter()
150 156 .map(|code| {
151 157 std::char::from_u32(*code)
152 158 .unwrap()
153 159 .encode_utf8(&mut [0; 3])
154 160 .bytes()
155 161 .collect()
156 162 })
157 163 .collect()
158 164 };
159 165 }
160 166
161 167 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
162 168 let mut buf = bytes.to_owned();
163 169 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
164 170 if needs_escaping {
165 171 for forbidden in IGNORED_CHARS.iter() {
166 172 replace_slice(&mut buf, forbidden, &[])
167 173 }
168 174 buf
169 175 } else {
170 176 buf
171 177 }
172 178 }
173 179
174 180 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
175 181 hfs_ignore_clean(&bytes.to_ascii_lowercase())
176 182 }
177 183
178 184 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
179 185 pub struct HgMetadata {
180 186 pub st_dev: u64,
181 187 pub st_mode: u32,
182 188 pub st_nlink: u64,
183 189 pub st_size: u64,
184 190 pub st_mtime: i64,
185 191 pub st_ctime: i64,
186 192 }
187 193
188 194 // TODO support other plaforms
189 195 #[cfg(unix)]
190 196 impl HgMetadata {
191 197 pub fn from_metadata(metadata: Metadata) -> Self {
192 198 use std::os::unix::fs::MetadataExt;
193 199 Self {
194 200 st_dev: metadata.dev(),
195 201 st_mode: metadata.mode(),
196 202 st_nlink: metadata.nlink(),
197 203 st_size: metadata.size(),
198 204 st_mtime: metadata.mtime(),
199 205 st_ctime: metadata.ctime(),
200 206 }
201 207 }
202 208
203 209 pub fn is_symlink(&self) -> bool {
204 210 // This is way too manual, but `HgMetadata` will go away in the
205 211 // near-future dirstate rewrite anyway.
206 212 self.st_mode & 0170000 == 0120000
207 213 }
208 214 }
209 215
210 216 /// Returns the canonical path of `name`, given `cwd` and `root`
211 217 pub fn canonical_path(
212 218 root: impl AsRef<Path>,
213 219 cwd: impl AsRef<Path>,
214 220 name: impl AsRef<Path>,
215 221 ) -> Result<PathBuf, HgPathError> {
216 222 // TODO add missing normalization for other platforms
217 223 let root = root.as_ref();
218 224 let cwd = cwd.as_ref();
219 225 let name = name.as_ref();
220 226
221 227 let name = if !name.is_absolute() {
222 228 root.join(&cwd).join(&name)
223 229 } else {
224 230 name.to_owned()
225 231 };
226 232 let auditor = PathAuditor::new(&root);
227 233 if name != root && name.starts_with(&root) {
228 234 let name = name.strip_prefix(&root).unwrap();
229 235 auditor.audit_path(path_to_hg_path_buf(name)?)?;
230 236 Ok(name.to_owned())
231 237 } else if name == root {
232 238 Ok("".into())
233 239 } else {
234 240 // Determine whether `name' is in the hierarchy at or beneath `root',
235 241 // by iterating name=name.parent() until it returns `None` (can't
236 242 // check name == '/', because that doesn't work on windows).
237 243 let mut name = name.deref();
238 244 let original_name = name.to_owned();
239 245 loop {
240 246 let same = is_same_file(&name, &root).unwrap_or(false);
241 247 if same {
242 248 if name == original_name {
243 249 // `name` was actually the same as root (maybe a symlink)
244 250 return Ok("".into());
245 251 }
246 252 // `name` is a symlink to root, so `original_name` is under
247 253 // root
248 254 let rel_path = original_name.strip_prefix(&name).unwrap();
249 255 auditor.audit_path(path_to_hg_path_buf(&rel_path)?)?;
250 256 return Ok(rel_path.to_owned());
251 257 }
252 258 name = match name.parent() {
253 259 None => break,
254 260 Some(p) => p,
255 261 };
256 262 }
257 263 // TODO hint to the user about using --cwd
258 264 // Bubble up the responsibility to Python for now
259 265 Err(HgPathError::NotUnderRoot {
260 266 path: original_name.to_owned(),
261 267 root: root.to_owned(),
262 268 })
263 269 }
264 270 }
265 271
266 272 /// Returns the representation of the path relative to the current working
267 273 /// directory for display purposes.
268 274 ///
269 275 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
270 276 /// of the repository.
271 277 ///
272 278 /// # Examples
273 279 ///
274 280 /// ```
275 281 /// use hg::utils::hg_path::HgPath;
276 282 /// use hg::utils::files::relativize_path;
277 283 /// use std::borrow::Cow;
278 284 ///
279 285 /// let file = HgPath::new(b"nested/file");
280 286 /// let cwd = HgPath::new(b"");
281 287 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
282 288 ///
283 289 /// let cwd = HgPath::new(b"nested");
284 290 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
285 291 ///
286 292 /// let cwd = HgPath::new(b"other");
287 293 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
288 294 /// ```
289 295 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
290 296 if cwd.as_ref().is_empty() {
291 297 Cow::Borrowed(path.as_bytes())
292 298 } else {
293 299 // This is not all accurate as to how large `res` will actually be, but
294 300 // profiling `rhg files` on a large-ish repo shows it’s better than
295 301 // starting from a zero-capacity `Vec` and letting `extend` reallocate
296 302 // repeatedly.
297 303 let guesstimate = path.as_bytes().len();
298 304
299 305 let mut res: Vec<u8> = Vec::with_capacity(guesstimate);
300 306 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
301 307 let mut cwd_iter =
302 308 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
303 309 loop {
304 310 match (path_iter.peek(), cwd_iter.peek()) {
305 311 (Some(a), Some(b)) if a == b => (),
306 312 _ => break,
307 313 }
308 314 path_iter.next();
309 315 cwd_iter.next();
310 316 }
311 317 let mut need_sep = false;
312 318 for _ in cwd_iter {
313 319 if need_sep {
314 320 res.extend(b"/")
315 321 } else {
316 322 need_sep = true
317 323 };
318 324 res.extend(b"..");
319 325 }
320 326 for c in path_iter {
321 327 if need_sep {
322 328 res.extend(b"/")
323 329 } else {
324 330 need_sep = true
325 331 };
326 332 res.extend(c);
327 333 }
328 334 Cow::Owned(res)
329 335 }
330 336 }
331 337
332 338 #[cfg(test)]
333 339 mod tests {
334 340 use super::*;
335 341 use pretty_assertions::assert_eq;
336 342
337 343 #[test]
338 344 fn find_dirs_some() {
339 345 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
340 346 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
341 347 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
342 348 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
343 349 assert_eq!(dirs.next(), None);
344 350 assert_eq!(dirs.next(), None);
345 351 }
346 352
347 353 #[test]
348 354 fn find_dirs_empty() {
349 355 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
350 356 let mut dirs = super::find_dirs(HgPath::new(b""));
351 357 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
352 358 assert_eq!(dirs.next(), None);
353 359 assert_eq!(dirs.next(), None);
354 360 }
355 361
356 362 #[test]
357 363 fn test_find_dirs_with_base_some() {
358 364 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
359 365 assert_eq!(
360 366 dirs.next(),
361 367 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
362 368 );
363 369 assert_eq!(
364 370 dirs.next(),
365 371 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
366 372 );
367 373 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
368 374 assert_eq!(dirs.next(), None);
369 375 assert_eq!(dirs.next(), None);
370 376 }
371 377
372 378 #[test]
373 379 fn test_find_dirs_with_base_empty() {
374 380 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
375 381 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
376 382 assert_eq!(dirs.next(), None);
377 383 assert_eq!(dirs.next(), None);
378 384 }
379 385
380 386 #[test]
381 387 fn test_canonical_path() {
382 388 let root = Path::new("/repo");
383 389 let cwd = Path::new("/dir");
384 390 let name = Path::new("filename");
385 391 assert_eq!(
386 392 canonical_path(root, cwd, name),
387 393 Err(HgPathError::NotUnderRoot {
388 394 path: PathBuf::from("/dir/filename"),
389 395 root: root.to_path_buf()
390 396 })
391 397 );
392 398
393 399 let root = Path::new("/repo");
394 400 let cwd = Path::new("/");
395 401 let name = Path::new("filename");
396 402 assert_eq!(
397 403 canonical_path(root, cwd, name),
398 404 Err(HgPathError::NotUnderRoot {
399 405 path: PathBuf::from("/filename"),
400 406 root: root.to_path_buf()
401 407 })
402 408 );
403 409
404 410 let root = Path::new("/repo");
405 411 let cwd = Path::new("/");
406 412 let name = Path::new("repo/filename");
407 413 assert_eq!(
408 414 canonical_path(root, cwd, name),
409 415 Ok(PathBuf::from("filename"))
410 416 );
411 417
412 418 let root = Path::new("/repo");
413 419 let cwd = Path::new("/repo");
414 420 let name = Path::new("filename");
415 421 assert_eq!(
416 422 canonical_path(root, cwd, name),
417 423 Ok(PathBuf::from("filename"))
418 424 );
419 425
420 426 let root = Path::new("/repo");
421 427 let cwd = Path::new("/repo/subdir");
422 428 let name = Path::new("filename");
423 429 assert_eq!(
424 430 canonical_path(root, cwd, name),
425 431 Ok(PathBuf::from("subdir/filename"))
426 432 );
427 433 }
428 434
429 435 #[test]
430 436 fn test_canonical_path_not_rooted() {
431 437 use std::fs::create_dir;
432 438 use tempfile::tempdir;
433 439
434 440 let base_dir = tempdir().unwrap();
435 441 let base_dir_path = base_dir.path();
436 442 let beneath_repo = base_dir_path.join("a");
437 443 let root = base_dir_path.join("a/b");
438 444 let out_of_repo = base_dir_path.join("c");
439 445 let under_repo_symlink = out_of_repo.join("d");
440 446
441 447 create_dir(&beneath_repo).unwrap();
442 448 create_dir(&root).unwrap();
443 449
444 450 // TODO make portable
445 451 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
446 452
447 453 assert_eq!(
448 454 canonical_path(&root, Path::new(""), out_of_repo),
449 455 Ok(PathBuf::from(""))
450 456 );
451 457 assert_eq!(
452 458 canonical_path(&root, Path::new(""), &beneath_repo),
453 459 Err(HgPathError::NotUnderRoot {
454 460 path: beneath_repo.to_owned(),
455 461 root: root.to_owned()
456 462 })
457 463 );
458 464 assert_eq!(
459 465 canonical_path(&root, Path::new(""), &under_repo_symlink),
460 466 Ok(PathBuf::from("d"))
461 467 );
462 468 }
463 469 }
@@ -1,785 +1,804 b''
1 1 // hg_path.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 4 //
5 5 // This software may be used and distributed according to the terms of the
6 6 // GNU General Public License version 2 or any later version.
7 7
8 8 use std::borrow::Borrow;
9 use std::borrow::Cow;
9 10 use std::convert::TryFrom;
10 11 use std::ffi::{OsStr, OsString};
11 12 use std::fmt;
12 13 use std::ops::Deref;
13 14 use std::path::{Path, PathBuf};
14 15
15 16 #[derive(Debug, Eq, PartialEq)]
16 17 pub enum HgPathError {
17 18 /// Bytes from the invalid `HgPath`
18 19 LeadingSlash(Vec<u8>),
19 20 ConsecutiveSlashes {
20 21 bytes: Vec<u8>,
21 22 second_slash_index: usize,
22 23 },
23 24 ContainsNullByte {
24 25 bytes: Vec<u8>,
25 26 null_byte_index: usize,
26 27 },
27 28 /// Bytes
28 29 DecodeError(Vec<u8>),
29 30 /// The rest come from audit errors
30 31 EndsWithSlash(HgPathBuf),
31 32 ContainsIllegalComponent(HgPathBuf),
32 33 /// Path is inside the `.hg` folder
33 34 InsideDotHg(HgPathBuf),
34 35 IsInsideNestedRepo {
35 36 path: HgPathBuf,
36 37 nested_repo: HgPathBuf,
37 38 },
38 39 TraversesSymbolicLink {
39 40 path: HgPathBuf,
40 41 symlink: HgPathBuf,
41 42 },
42 43 NotFsCompliant(HgPathBuf),
43 44 /// `path` is the smallest invalid path
44 45 NotUnderRoot {
45 46 path: PathBuf,
46 47 root: PathBuf,
47 48 },
48 49 }
49 50
50 51 impl fmt::Display for HgPathError {
51 52 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52 53 match self {
53 54 HgPathError::LeadingSlash(bytes) => {
54 55 write!(f, "Invalid HgPath '{:?}': has a leading slash.", bytes)
55 56 }
56 57 HgPathError::ConsecutiveSlashes {
57 58 bytes,
58 59 second_slash_index: pos,
59 60 } => write!(
60 61 f,
61 62 "Invalid HgPath '{:?}': consecutive slashes at pos {}.",
62 63 bytes, pos
63 64 ),
64 65 HgPathError::ContainsNullByte {
65 66 bytes,
66 67 null_byte_index: pos,
67 68 } => write!(
68 69 f,
69 70 "Invalid HgPath '{:?}': contains null byte at pos {}.",
70 71 bytes, pos
71 72 ),
72 73 HgPathError::DecodeError(bytes) => write!(
73 74 f,
74 75 "Invalid HgPath '{:?}': could not be decoded.",
75 76 bytes
76 77 ),
77 78 HgPathError::EndsWithSlash(path) => {
78 79 write!(f, "Audit failed for '{}': ends with a slash.", path)
79 80 }
80 81 HgPathError::ContainsIllegalComponent(path) => write!(
81 82 f,
82 83 "Audit failed for '{}': contains an illegal component.",
83 84 path
84 85 ),
85 86 HgPathError::InsideDotHg(path) => write!(
86 87 f,
87 88 "Audit failed for '{}': is inside the '.hg' folder.",
88 89 path
89 90 ),
90 91 HgPathError::IsInsideNestedRepo {
91 92 path,
92 93 nested_repo: nested,
93 94 } => {
94 95 write!(f,
95 96 "Audit failed for '{}': is inside a nested repository '{}'.",
96 97 path, nested
97 98 )
98 99 }
99 100 HgPathError::TraversesSymbolicLink { path, symlink } => write!(
100 101 f,
101 102 "Audit failed for '{}': traverses symbolic link '{}'.",
102 103 path, symlink
103 104 ),
104 105 HgPathError::NotFsCompliant(path) => write!(
105 106 f,
106 107 "Audit failed for '{}': cannot be turned into a \
107 108 filesystem path.",
108 109 path
109 110 ),
110 111 HgPathError::NotUnderRoot { path, root } => write!(
111 112 f,
112 113 "Audit failed for '{}': not under root {}.",
113 114 path.display(),
114 115 root.display()
115 116 ),
116 117 }
117 118 }
118 119 }
119 120
120 121 impl From<HgPathError> for std::io::Error {
121 122 fn from(e: HgPathError) -> Self {
122 123 std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())
123 124 }
124 125 }
125 126
126 127 /// This is a repository-relative path (or canonical path):
127 128 /// - no null characters
128 129 /// - `/` separates directories
129 130 /// - no consecutive slashes
130 131 /// - no leading slash,
131 132 /// - no `.` nor `..` of special meaning
132 133 /// - stored in repository and shared across platforms
133 134 ///
134 135 /// Note: there is no guarantee of any `HgPath` being well-formed at any point
135 136 /// in its lifetime for performance reasons and to ease ergonomics. It is
136 137 /// however checked using the `check_state` method before any file-system
137 138 /// operation.
138 139 ///
139 140 /// This allows us to be encoding-transparent as much as possible, until really
140 141 /// needed; `HgPath` can be transformed into a platform-specific path (`OsStr`
141 142 /// or `Path`) whenever more complex operations are needed:
142 143 /// On Unix, it's just byte-to-byte conversion. On Windows, it has to be
143 144 /// decoded from MBCS to WTF-8. If WindowsUTF8Plan is implemented, the source
144 145 /// character encoding will be determined on a per-repository basis.
145 146 //
146 147 // FIXME: (adapted from a comment in the stdlib)
147 148 // `HgPath::new()` current implementation relies on `Slice` being
148 149 // layout-compatible with `[u8]`.
149 150 // When attribute privacy is implemented, `Slice` should be annotated as
150 151 // `#[repr(transparent)]`.
151 152 // Anyway, `Slice` representation and layout are considered implementation
152 153 // detail, are not documented and must not be relied upon.
153 154 #[derive(Eq, Ord, PartialEq, PartialOrd, Hash)]
154 155 pub struct HgPath {
155 156 inner: [u8],
156 157 }
157 158
158 159 impl HgPath {
159 160 pub fn new<S: AsRef<[u8]> + ?Sized>(s: &S) -> &Self {
160 161 unsafe { &*(s.as_ref() as *const [u8] as *const Self) }
161 162 }
162 163 pub fn is_empty(&self) -> bool {
163 164 self.inner.is_empty()
164 165 }
165 166 pub fn len(&self) -> usize {
166 167 self.inner.len()
167 168 }
168 169 fn to_hg_path_buf(&self) -> HgPathBuf {
169 170 HgPathBuf {
170 171 inner: self.inner.to_owned(),
171 172 }
172 173 }
173 174 pub fn bytes(&self) -> std::slice::Iter<u8> {
174 175 self.inner.iter()
175 176 }
176 177 pub fn to_ascii_uppercase(&self) -> HgPathBuf {
177 178 HgPathBuf::from(self.inner.to_ascii_uppercase())
178 179 }
179 180 pub fn to_ascii_lowercase(&self) -> HgPathBuf {
180 181 HgPathBuf::from(self.inner.to_ascii_lowercase())
181 182 }
182 183 pub fn as_bytes(&self) -> &[u8] {
183 184 &self.inner
184 185 }
185 186 pub fn contains(&self, other: u8) -> bool {
186 187 self.inner.contains(&other)
187 188 }
188 189 pub fn starts_with(&self, needle: impl AsRef<Self>) -> bool {
189 190 self.inner.starts_with(needle.as_ref().as_bytes())
190 191 }
191 192 pub fn trim_trailing_slash(&self) -> &Self {
192 193 Self::new(if self.inner.last() == Some(&b'/') {
193 194 &self.inner[..self.inner.len() - 1]
194 195 } else {
195 196 &self.inner[..]
196 197 })
197 198 }
198 199 /// Returns a tuple of slices `(base, filename)` resulting from the split
199 200 /// at the rightmost `/`, if any.
200 201 ///
201 202 /// # Examples:
202 203 ///
203 204 /// ```
204 205 /// use hg::utils::hg_path::HgPath;
205 206 ///
206 207 /// let path = HgPath::new(b"cool/hg/path").split_filename();
207 208 /// assert_eq!(path, (HgPath::new(b"cool/hg"), HgPath::new(b"path")));
208 209 ///
209 210 /// let path = HgPath::new(b"pathwithoutsep").split_filename();
210 211 /// assert_eq!(path, (HgPath::new(b""), HgPath::new(b"pathwithoutsep")));
211 212 /// ```
212 213 pub fn split_filename(&self) -> (&Self, &Self) {
213 214 match &self.inner.iter().rposition(|c| *c == b'/') {
214 215 None => (HgPath::new(""), &self),
215 216 Some(size) => (
216 217 HgPath::new(&self.inner[..*size]),
217 218 HgPath::new(&self.inner[*size + 1..]),
218 219 ),
219 220 }
220 221 }
221 222 pub fn join<T: ?Sized + AsRef<Self>>(&self, other: &T) -> HgPathBuf {
222 223 let mut inner = self.inner.to_owned();
223 224 if !inner.is_empty() && inner.last() != Some(&b'/') {
224 225 inner.push(b'/');
225 226 }
226 227 inner.extend(other.as_ref().bytes());
227 228 HgPathBuf::from_bytes(&inner)
228 229 }
229 230
230 231 pub fn components(&self) -> impl Iterator<Item = &HgPath> {
231 232 self.inner.split(|&byte| byte == b'/').map(HgPath::new)
232 233 }
233 234
234 235 pub fn parent(&self) -> &Self {
235 236 let inner = self.as_bytes();
236 237 HgPath::new(match inner.iter().rposition(|b| *b == b'/') {
237 238 Some(pos) => &inner[..pos],
238 239 None => &[],
239 240 })
240 241 }
241 242 /// Given a base directory, returns the slice of `self` relative to the
242 243 /// base directory. If `base` is not a directory (does not end with a
243 244 /// `b'/'`), returns `None`.
244 245 pub fn relative_to(&self, base: impl AsRef<Self>) -> Option<&Self> {
245 246 let base = base.as_ref();
246 247 if base.is_empty() {
247 248 return Some(self);
248 249 }
249 250 let is_dir = base.as_bytes().ends_with(b"/");
250 251 if is_dir && self.starts_with(base) {
251 252 Some(Self::new(&self.inner[base.len()..]))
252 253 } else {
253 254 None
254 255 }
255 256 }
256 257
257 258 #[cfg(windows)]
258 259 /// Copied from the Python stdlib's `os.path.splitdrive` implementation.
259 260 ///
260 261 /// Split a pathname into drive/UNC sharepoint and relative path
261 262 /// specifiers. Returns a 2-tuple (drive_or_unc, path); either part may
262 263 /// be empty.
263 264 ///
264 265 /// If you assign
265 266 /// result = split_drive(p)
266 267 /// It is always true that:
267 268 /// result[0] + result[1] == p
268 269 ///
269 270 /// If the path contained a drive letter, drive_or_unc will contain
270 271 /// everything up to and including the colon.
271 272 /// e.g. split_drive("c:/dir") returns ("c:", "/dir")
272 273 ///
273 274 /// If the path contained a UNC path, the drive_or_unc will contain the
274 275 /// host name and share up to but not including the fourth directory
275 276 /// separator character.
276 277 /// e.g. split_drive("//host/computer/dir") returns ("//host/computer",
277 278 /// "/dir")
278 279 ///
279 280 /// Paths cannot contain both a drive letter and a UNC path.
280 281 pub fn split_drive<'a>(&self) -> (&HgPath, &HgPath) {
281 282 let bytes = self.as_bytes();
282 283 let is_sep = |b| std::path::is_separator(b as char);
283 284
284 285 if self.len() < 2 {
285 286 (HgPath::new(b""), &self)
286 287 } else if is_sep(bytes[0])
287 288 && is_sep(bytes[1])
288 289 && (self.len() == 2 || !is_sep(bytes[2]))
289 290 {
290 291 // Is a UNC path:
291 292 // vvvvvvvvvvvvvvvvvvvv drive letter or UNC path
292 293 // \\machine\mountpoint\directory\etc\...
293 294 // directory ^^^^^^^^^^^^^^^
294 295
295 296 let machine_end_index = bytes[2..].iter().position(|b| is_sep(*b));
296 297 let mountpoint_start_index = if let Some(i) = machine_end_index {
297 298 i + 2
298 299 } else {
299 300 return (HgPath::new(b""), &self);
300 301 };
301 302
302 303 match bytes[mountpoint_start_index + 1..]
303 304 .iter()
304 305 .position(|b| is_sep(*b))
305 306 {
306 307 // A UNC path can't have two slashes in a row
307 308 // (after the initial two)
308 309 Some(0) => (HgPath::new(b""), &self),
309 310 Some(i) => {
310 311 let (a, b) =
311 312 bytes.split_at(mountpoint_start_index + 1 + i);
312 313 (HgPath::new(a), HgPath::new(b))
313 314 }
314 315 None => (&self, HgPath::new(b"")),
315 316 }
316 317 } else if bytes[1] == b':' {
317 318 // Drive path c:\directory
318 319 let (a, b) = bytes.split_at(2);
319 320 (HgPath::new(a), HgPath::new(b))
320 321 } else {
321 322 (HgPath::new(b""), &self)
322 323 }
323 324 }
324 325
325 326 #[cfg(unix)]
326 327 /// Split a pathname into drive and path. On Posix, drive is always empty.
327 328 pub fn split_drive(&self) -> (&HgPath, &HgPath) {
328 329 (HgPath::new(b""), &self)
329 330 }
330 331
331 332 /// Checks for errors in the path, short-circuiting at the first one.
332 333 /// This generates fine-grained errors useful for debugging.
333 334 /// To simply check if the path is valid during tests, use `is_valid`.
334 335 pub fn check_state(&self) -> Result<(), HgPathError> {
335 336 if self.is_empty() {
336 337 return Ok(());
337 338 }
338 339 let bytes = self.as_bytes();
339 340 let mut previous_byte = None;
340 341
341 342 if bytes[0] == b'/' {
342 343 return Err(HgPathError::LeadingSlash(bytes.to_vec()));
343 344 }
344 345 for (index, byte) in bytes.iter().enumerate() {
345 346 match byte {
346 347 0 => {
347 348 return Err(HgPathError::ContainsNullByte {
348 349 bytes: bytes.to_vec(),
349 350 null_byte_index: index,
350 351 })
351 352 }
352 353 b'/' => {
353 354 if previous_byte.is_some() && previous_byte == Some(b'/') {
354 355 return Err(HgPathError::ConsecutiveSlashes {
355 356 bytes: bytes.to_vec(),
356 357 second_slash_index: index,
357 358 });
358 359 }
359 360 }
360 361 _ => (),
361 362 };
362 363 previous_byte = Some(*byte);
363 364 }
364 365 Ok(())
365 366 }
366 367
367 368 #[cfg(test)]
368 369 /// Only usable during tests to force developers to handle invalid states
369 370 fn is_valid(&self) -> bool {
370 371 self.check_state().is_ok()
371 372 }
372 373 }
373 374
374 375 impl fmt::Debug for HgPath {
375 376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376 377 write!(f, "HgPath({:?})", String::from_utf8_lossy(&self.inner))
377 378 }
378 379 }
379 380
380 381 impl fmt::Display for HgPath {
381 382 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 383 write!(f, "{}", String::from_utf8_lossy(&self.inner))
383 384 }
384 385 }
385 386
386 387 #[derive(
387 388 Default, Eq, Ord, Clone, PartialEq, PartialOrd, Hash, derive_more::From,
388 389 )]
389 390 pub struct HgPathBuf {
390 391 inner: Vec<u8>,
391 392 }
392 393
393 394 impl HgPathBuf {
394 395 pub fn new() -> Self {
395 396 Default::default()
396 397 }
397 398 pub fn push(&mut self, byte: u8) {
398 399 self.inner.push(byte);
399 400 }
400 401 pub fn from_bytes(s: &[u8]) -> HgPathBuf {
401 402 HgPath::new(s).to_owned()
402 403 }
403 404 pub fn into_vec(self) -> Vec<u8> {
404 405 self.inner
405 406 }
406 407 }
407 408
408 409 impl fmt::Debug for HgPathBuf {
409 410 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 411 write!(f, "HgPathBuf({:?})", String::from_utf8_lossy(&self.inner))
411 412 }
412 413 }
413 414
414 415 impl fmt::Display for HgPathBuf {
415 416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
416 417 write!(f, "{}", String::from_utf8_lossy(&self.inner))
417 418 }
418 419 }
419 420
420 421 impl Deref for HgPathBuf {
421 422 type Target = HgPath;
422 423
423 424 #[inline]
424 425 fn deref(&self) -> &HgPath {
425 426 &HgPath::new(&self.inner)
426 427 }
427 428 }
428 429
429 430 impl<T: ?Sized + AsRef<HgPath>> From<&T> for HgPathBuf {
430 431 fn from(s: &T) -> HgPathBuf {
431 432 s.as_ref().to_owned()
432 433 }
433 434 }
434 435
435 436 impl Into<Vec<u8>> for HgPathBuf {
436 437 fn into(self) -> Vec<u8> {
437 438 self.inner
438 439 }
439 440 }
440 441
441 442 impl Borrow<HgPath> for HgPathBuf {
442 443 fn borrow(&self) -> &HgPath {
443 444 &HgPath::new(self.as_bytes())
444 445 }
445 446 }
446 447
447 448 impl ToOwned for HgPath {
448 449 type Owned = HgPathBuf;
449 450
450 451 fn to_owned(&self) -> HgPathBuf {
451 452 self.to_hg_path_buf()
452 453 }
453 454 }
454 455
455 456 impl AsRef<HgPath> for HgPath {
456 457 fn as_ref(&self) -> &HgPath {
457 458 self
458 459 }
459 460 }
460 461
461 462 impl AsRef<HgPath> for HgPathBuf {
462 463 fn as_ref(&self) -> &HgPath {
463 464 self
464 465 }
465 466 }
466 467
467 468 impl Extend<u8> for HgPathBuf {
468 469 fn extend<T: IntoIterator<Item = u8>>(&mut self, iter: T) {
469 470 self.inner.extend(iter);
470 471 }
471 472 }
472 473
473 474 /// TODO: Once https://www.mercurial-scm.org/wiki/WindowsUTF8Plan is
474 475 /// implemented, these conversion utils will have to work differently depending
475 476 /// on the repository encoding: either `UTF-8` or `MBCS`.
476 477
477 478 pub fn hg_path_to_os_string<P: AsRef<HgPath>>(
478 479 hg_path: P,
479 480 ) -> Result<OsString, HgPathError> {
480 481 hg_path.as_ref().check_state()?;
481 482 let os_str;
482 483 #[cfg(unix)]
483 484 {
484 485 use std::os::unix::ffi::OsStrExt;
485 486 os_str = std::ffi::OsStr::from_bytes(&hg_path.as_ref().as_bytes());
486 487 }
487 488 // TODO Handle other platforms
488 489 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
489 490 Ok(os_str.to_os_string())
490 491 }
491 492
492 493 pub fn hg_path_to_path_buf<P: AsRef<HgPath>>(
493 494 hg_path: P,
494 495 ) -> Result<PathBuf, HgPathError> {
495 496 Ok(Path::new(&hg_path_to_os_string(hg_path)?).to_path_buf())
496 497 }
497 498
498 499 pub fn os_string_to_hg_path_buf<S: AsRef<OsStr>>(
499 500 os_string: S,
500 501 ) -> Result<HgPathBuf, HgPathError> {
501 502 let buf;
502 503 #[cfg(unix)]
503 504 {
504 505 use std::os::unix::ffi::OsStrExt;
505 506 buf = HgPathBuf::from_bytes(&os_string.as_ref().as_bytes());
506 507 }
507 508 // TODO Handle other platforms
508 509 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
509 510
510 511 buf.check_state()?;
511 512 Ok(buf)
512 513 }
513 514
514 515 pub fn path_to_hg_path_buf<P: AsRef<Path>>(
515 516 path: P,
516 517 ) -> Result<HgPathBuf, HgPathError> {
517 518 let buf;
518 519 let os_str = path.as_ref().as_os_str();
519 520 #[cfg(unix)]
520 521 {
521 522 use std::os::unix::ffi::OsStrExt;
522 523 buf = HgPathBuf::from_bytes(&os_str.as_bytes());
523 524 }
524 525 // TODO Handle other platforms
525 526 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
526 527
527 528 buf.check_state()?;
528 529 Ok(buf)
529 530 }
530 531
531 532 impl TryFrom<PathBuf> for HgPathBuf {
532 533 type Error = HgPathError;
533 534 fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
534 535 path_to_hg_path_buf(path)
535 536 }
536 537 }
537 538
539 impl From<HgPathBuf> for Cow<'_, HgPath> {
540 fn from(path: HgPathBuf) -> Self {
541 Cow::Owned(path)
542 }
543 }
544
545 impl<'a> From<&'a HgPath> for Cow<'a, HgPath> {
546 fn from(path: &'a HgPath) -> Self {
547 Cow::Borrowed(path)
548 }
549 }
550
551 impl<'a> From<&'a HgPathBuf> for Cow<'a, HgPath> {
552 fn from(path: &'a HgPathBuf) -> Self {
553 Cow::Borrowed(&**path)
554 }
555 }
556
538 557 #[cfg(test)]
539 558 mod tests {
540 559 use super::*;
541 560 use pretty_assertions::assert_eq;
542 561
543 562 #[test]
544 563 fn test_path_states() {
545 564 assert_eq!(
546 565 Err(HgPathError::LeadingSlash(b"/".to_vec())),
547 566 HgPath::new(b"/").check_state()
548 567 );
549 568 assert_eq!(
550 569 Err(HgPathError::ConsecutiveSlashes {
551 570 bytes: b"a/b//c".to_vec(),
552 571 second_slash_index: 4
553 572 }),
554 573 HgPath::new(b"a/b//c").check_state()
555 574 );
556 575 assert_eq!(
557 576 Err(HgPathError::ContainsNullByte {
558 577 bytes: b"a/b/\0c".to_vec(),
559 578 null_byte_index: 4
560 579 }),
561 580 HgPath::new(b"a/b/\0c").check_state()
562 581 );
563 582 // TODO test HgPathError::DecodeError for the Windows implementation.
564 583 assert_eq!(true, HgPath::new(b"").is_valid());
565 584 assert_eq!(true, HgPath::new(b"a/b/c").is_valid());
566 585 // Backslashes in paths are not significant, but allowed
567 586 assert_eq!(true, HgPath::new(br"a\b/c").is_valid());
568 587 // Dots in paths are not significant, but allowed
569 588 assert_eq!(true, HgPath::new(b"a/b/../c/").is_valid());
570 589 assert_eq!(true, HgPath::new(b"./a/b/../c/").is_valid());
571 590 }
572 591
573 592 #[test]
574 593 fn test_iter() {
575 594 let path = HgPath::new(b"a");
576 595 let mut iter = path.bytes();
577 596 assert_eq!(Some(&b'a'), iter.next());
578 597 assert_eq!(None, iter.next_back());
579 598 assert_eq!(None, iter.next());
580 599
581 600 let path = HgPath::new(b"a");
582 601 let mut iter = path.bytes();
583 602 assert_eq!(Some(&b'a'), iter.next_back());
584 603 assert_eq!(None, iter.next_back());
585 604 assert_eq!(None, iter.next());
586 605
587 606 let path = HgPath::new(b"abc");
588 607 let mut iter = path.bytes();
589 608 assert_eq!(Some(&b'a'), iter.next());
590 609 assert_eq!(Some(&b'c'), iter.next_back());
591 610 assert_eq!(Some(&b'b'), iter.next_back());
592 611 assert_eq!(None, iter.next_back());
593 612 assert_eq!(None, iter.next());
594 613
595 614 let path = HgPath::new(b"abc");
596 615 let mut iter = path.bytes();
597 616 assert_eq!(Some(&b'a'), iter.next());
598 617 assert_eq!(Some(&b'b'), iter.next());
599 618 assert_eq!(Some(&b'c'), iter.next());
600 619 assert_eq!(None, iter.next_back());
601 620 assert_eq!(None, iter.next());
602 621
603 622 let path = HgPath::new(b"abc");
604 623 let iter = path.bytes();
605 624 let mut vec = Vec::new();
606 625 vec.extend(iter);
607 626 assert_eq!(vec![b'a', b'b', b'c'], vec);
608 627
609 628 let path = HgPath::new(b"abc");
610 629 let mut iter = path.bytes();
611 630 assert_eq!(Some(2), iter.rposition(|c| *c == b'c'));
612 631
613 632 let path = HgPath::new(b"abc");
614 633 let mut iter = path.bytes();
615 634 assert_eq!(None, iter.rposition(|c| *c == b'd'));
616 635 }
617 636
618 637 #[test]
619 638 fn test_join() {
620 639 let path = HgPathBuf::from_bytes(b"a").join(HgPath::new(b"b"));
621 640 assert_eq!(b"a/b", path.as_bytes());
622 641
623 642 let path = HgPathBuf::from_bytes(b"a/").join(HgPath::new(b"b/c"));
624 643 assert_eq!(b"a/b/c", path.as_bytes());
625 644
626 645 // No leading slash if empty before join
627 646 let path = HgPathBuf::new().join(HgPath::new(b"b/c"));
628 647 assert_eq!(b"b/c", path.as_bytes());
629 648
630 649 // The leading slash is an invalid representation of an `HgPath`, but
631 650 // it can happen. This creates another invalid representation of
632 651 // consecutive bytes.
633 652 // TODO What should be done in this case? Should we silently remove
634 653 // the extra slash? Should we change the signature to a problematic
635 654 // `Result<HgPathBuf, HgPathError>`, or should we just keep it so and
636 655 // let the error happen upon filesystem interaction?
637 656 let path = HgPathBuf::from_bytes(b"a/").join(HgPath::new(b"/b"));
638 657 assert_eq!(b"a//b", path.as_bytes());
639 658 let path = HgPathBuf::from_bytes(b"a").join(HgPath::new(b"/b"));
640 659 assert_eq!(b"a//b", path.as_bytes());
641 660 }
642 661
643 662 #[test]
644 663 fn test_relative_to() {
645 664 let path = HgPath::new(b"");
646 665 let base = HgPath::new(b"");
647 666 assert_eq!(Some(path), path.relative_to(base));
648 667
649 668 let path = HgPath::new(b"path");
650 669 let base = HgPath::new(b"");
651 670 assert_eq!(Some(path), path.relative_to(base));
652 671
653 672 let path = HgPath::new(b"a");
654 673 let base = HgPath::new(b"b");
655 674 assert_eq!(None, path.relative_to(base));
656 675
657 676 let path = HgPath::new(b"a/b");
658 677 let base = HgPath::new(b"a");
659 678 assert_eq!(None, path.relative_to(base));
660 679
661 680 let path = HgPath::new(b"a/b");
662 681 let base = HgPath::new(b"a/");
663 682 assert_eq!(Some(HgPath::new(b"b")), path.relative_to(base));
664 683
665 684 let path = HgPath::new(b"nested/path/to/b");
666 685 let base = HgPath::new(b"nested/path/");
667 686 assert_eq!(Some(HgPath::new(b"to/b")), path.relative_to(base));
668 687
669 688 let path = HgPath::new(b"ends/with/dir/");
670 689 let base = HgPath::new(b"ends/");
671 690 assert_eq!(Some(HgPath::new(b"with/dir/")), path.relative_to(base));
672 691 }
673 692
674 693 #[test]
675 694 #[cfg(unix)]
676 695 fn test_split_drive() {
677 696 // Taken from the Python stdlib's tests
678 697 assert_eq!(
679 698 HgPath::new(br"/foo/bar").split_drive(),
680 699 (HgPath::new(b""), HgPath::new(br"/foo/bar"))
681 700 );
682 701 assert_eq!(
683 702 HgPath::new(br"foo:bar").split_drive(),
684 703 (HgPath::new(b""), HgPath::new(br"foo:bar"))
685 704 );
686 705 assert_eq!(
687 706 HgPath::new(br":foo:bar").split_drive(),
688 707 (HgPath::new(b""), HgPath::new(br":foo:bar"))
689 708 );
690 709 // Also try NT paths; should not split them
691 710 assert_eq!(
692 711 HgPath::new(br"c:\foo\bar").split_drive(),
693 712 (HgPath::new(b""), HgPath::new(br"c:\foo\bar"))
694 713 );
695 714 assert_eq!(
696 715 HgPath::new(b"c:/foo/bar").split_drive(),
697 716 (HgPath::new(b""), HgPath::new(br"c:/foo/bar"))
698 717 );
699 718 assert_eq!(
700 719 HgPath::new(br"\\conky\mountpoint\foo\bar").split_drive(),
701 720 (
702 721 HgPath::new(b""),
703 722 HgPath::new(br"\\conky\mountpoint\foo\bar")
704 723 )
705 724 );
706 725 }
707 726
708 727 #[test]
709 728 #[cfg(windows)]
710 729 fn test_split_drive() {
711 730 assert_eq!(
712 731 HgPath::new(br"c:\foo\bar").split_drive(),
713 732 (HgPath::new(br"c:"), HgPath::new(br"\foo\bar"))
714 733 );
715 734 assert_eq!(
716 735 HgPath::new(b"c:/foo/bar").split_drive(),
717 736 (HgPath::new(br"c:"), HgPath::new(br"/foo/bar"))
718 737 );
719 738 assert_eq!(
720 739 HgPath::new(br"\\conky\mountpoint\foo\bar").split_drive(),
721 740 (
722 741 HgPath::new(br"\\conky\mountpoint"),
723 742 HgPath::new(br"\foo\bar")
724 743 )
725 744 );
726 745 assert_eq!(
727 746 HgPath::new(br"//conky/mountpoint/foo/bar").split_drive(),
728 747 (
729 748 HgPath::new(br"//conky/mountpoint"),
730 749 HgPath::new(br"/foo/bar")
731 750 )
732 751 );
733 752 assert_eq!(
734 753 HgPath::new(br"\\\conky\mountpoint\foo\bar").split_drive(),
735 754 (
736 755 HgPath::new(br""),
737 756 HgPath::new(br"\\\conky\mountpoint\foo\bar")
738 757 )
739 758 );
740 759 assert_eq!(
741 760 HgPath::new(br"///conky/mountpoint/foo/bar").split_drive(),
742 761 (
743 762 HgPath::new(br""),
744 763 HgPath::new(br"///conky/mountpoint/foo/bar")
745 764 )
746 765 );
747 766 assert_eq!(
748 767 HgPath::new(br"\\conky\\mountpoint\foo\bar").split_drive(),
749 768 (
750 769 HgPath::new(br""),
751 770 HgPath::new(br"\\conky\\mountpoint\foo\bar")
752 771 )
753 772 );
754 773 assert_eq!(
755 774 HgPath::new(br"//conky//mountpoint/foo/bar").split_drive(),
756 775 (
757 776 HgPath::new(br""),
758 777 HgPath::new(br"//conky//mountpoint/foo/bar")
759 778 )
760 779 );
761 780 // UNC part containing U+0130
762 781 assert_eq!(
763 782 HgPath::new(b"//conky/MOUNTPO\xc4\xb0NT/foo/bar").split_drive(),
764 783 (
765 784 HgPath::new(b"//conky/MOUNTPO\xc4\xb0NT"),
766 785 HgPath::new(br"/foo/bar")
767 786 )
768 787 );
769 788 }
770 789
771 790 #[test]
772 791 fn test_parent() {
773 792 let path = HgPath::new(b"");
774 793 assert_eq!(path.parent(), path);
775 794
776 795 let path = HgPath::new(b"a");
777 796 assert_eq!(path.parent(), HgPath::new(b""));
778 797
779 798 let path = HgPath::new(b"a/b");
780 799 assert_eq!(path.parent(), HgPath::new(b"a"));
781 800
782 801 let path = HgPath::new(b"a/other/b");
783 802 assert_eq!(path.parent(), HgPath::new(b"a/other"));
784 803 }
785 804 }
General Comments 0
You need to be logged in to leave comments. Login now