##// END OF EJS Templates
rust-status: use bare hg status fastpath from Python...
Raphaël Gomès -
r45017:4d1634e5 default
parent child Browse files
Show More
@@ -1,1866 +1,1901 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from .pycompat import delattr
19 19
20 20 from hgdemandimport import tracing
21 21
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 sparse,
30 31 txnutil,
31 32 util,
32 33 )
33 34
34 35 from .interfaces import (
35 36 dirstate as intdirstate,
36 37 util as interfaceutil,
37 38 )
38 39
39 40 parsers = policy.importmod('parsers')
40 41 rustmod = policy.importrust('dirstate')
41 42
42 43 propertycache = util.propertycache
43 44 filecache = scmutil.filecache
44 45 _rangemask = 0x7FFFFFFF
45 46
46 47 dirstatetuple = parsers.dirstatetuple
47 48
48 49
49 50 class repocache(filecache):
50 51 """filecache for files in .hg/"""
51 52
52 53 def join(self, obj, fname):
53 54 return obj._opener.join(fname)
54 55
55 56
56 57 class rootcache(filecache):
57 58 """filecache for files in the repository root"""
58 59
59 60 def join(self, obj, fname):
60 61 return obj._join(fname)
61 62
62 63
63 64 def _getfsnow(vfs):
64 65 '''Get "now" timestamp on filesystem'''
65 66 tmpfd, tmpname = vfs.mkstemp()
66 67 try:
67 68 return os.fstat(tmpfd)[stat.ST_MTIME]
68 69 finally:
69 70 os.close(tmpfd)
70 71 vfs.unlink(tmpname)
71 72
72 73
73 74 @interfaceutil.implementer(intdirstate.idirstate)
74 75 class dirstate(object):
75 76 def __init__(self, opener, ui, root, validate, sparsematchfn):
76 77 '''Create a new dirstate object.
77 78
78 79 opener is an open()-like callable that can be used to open the
79 80 dirstate file; root is the root of the directory tracked by
80 81 the dirstate.
81 82 '''
82 83 self._opener = opener
83 84 self._validate = validate
84 85 self._root = root
85 86 self._sparsematchfn = sparsematchfn
86 87 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
87 88 # UNC path pointing to root share (issue4557)
88 89 self._rootdir = pathutil.normasprefix(root)
89 90 self._dirty = False
90 91 self._lastnormaltime = 0
91 92 self._ui = ui
92 93 self._filecache = {}
93 94 self._parentwriters = 0
94 95 self._filename = b'dirstate'
95 96 self._pendingfilename = b'%s.pending' % self._filename
96 97 self._plchangecallbacks = {}
97 98 self._origpl = None
98 99 self._updatedfiles = set()
99 100 self._mapcls = dirstatemap
100 101 # Access and cache cwd early, so we don't access it for the first time
101 102 # after a working-copy update caused it to not exist (accessing it then
102 103 # raises an exception).
103 104 self._cwd
104 105
105 106 @contextlib.contextmanager
106 107 def parentchange(self):
107 108 '''Context manager for handling dirstate parents.
108 109
109 110 If an exception occurs in the scope of the context manager,
110 111 the incoherent dirstate won't be written when wlock is
111 112 released.
112 113 '''
113 114 self._parentwriters += 1
114 115 yield
115 116 # Typically we want the "undo" step of a context manager in a
116 117 # finally block so it happens even when an exception
117 118 # occurs. In this case, however, we only want to decrement
118 119 # parentwriters if the code in the with statement exits
119 120 # normally, so we don't have a try/finally here on purpose.
120 121 self._parentwriters -= 1
121 122
122 123 def pendingparentchange(self):
123 124 '''Returns true if the dirstate is in the middle of a set of changes
124 125 that modify the dirstate parent.
125 126 '''
126 127 return self._parentwriters > 0
127 128
128 129 @propertycache
129 130 def _map(self):
130 131 """Return the dirstate contents (see documentation for dirstatemap)."""
131 132 self._map = self._mapcls(self._ui, self._opener, self._root)
132 133 return self._map
133 134
134 135 @property
135 136 def _sparsematcher(self):
136 137 """The matcher for the sparse checkout.
137 138
138 139 The working directory may not include every file from a manifest. The
139 140 matcher obtained by this property will match a path if it is to be
140 141 included in the working directory.
141 142 """
142 143 # TODO there is potential to cache this property. For now, the matcher
143 144 # is resolved on every access. (But the called function does use a
144 145 # cache to keep the lookup fast.)
145 146 return self._sparsematchfn()
146 147
147 148 @repocache(b'branch')
148 149 def _branch(self):
149 150 try:
150 151 return self._opener.read(b"branch").strip() or b"default"
151 152 except IOError as inst:
152 153 if inst.errno != errno.ENOENT:
153 154 raise
154 155 return b"default"
155 156
156 157 @property
157 158 def _pl(self):
158 159 return self._map.parents()
159 160
160 161 def hasdir(self, d):
161 162 return self._map.hastrackeddir(d)
162 163
163 164 @rootcache(b'.hgignore')
164 165 def _ignore(self):
165 166 files = self._ignorefiles()
166 167 if not files:
167 168 return matchmod.never()
168 169
169 170 pats = [b'include:%s' % f for f in files]
170 171 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
171 172
172 173 @propertycache
173 174 def _slash(self):
174 175 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
175 176
176 177 @propertycache
177 178 def _checklink(self):
178 179 return util.checklink(self._root)
179 180
180 181 @propertycache
181 182 def _checkexec(self):
182 183 return util.checkexec(self._root)
183 184
184 185 @propertycache
185 186 def _checkcase(self):
186 187 return not util.fscasesensitive(self._join(b'.hg'))
187 188
188 189 def _join(self, f):
189 190 # much faster than os.path.join()
190 191 # it's safe because f is always a relative path
191 192 return self._rootdir + f
192 193
193 194 def flagfunc(self, buildfallback):
194 195 if self._checklink and self._checkexec:
195 196
196 197 def f(x):
197 198 try:
198 199 st = os.lstat(self._join(x))
199 200 if util.statislink(st):
200 201 return b'l'
201 202 if util.statisexec(st):
202 203 return b'x'
203 204 except OSError:
204 205 pass
205 206 return b''
206 207
207 208 return f
208 209
209 210 fallback = buildfallback()
210 211 if self._checklink:
211 212
212 213 def f(x):
213 214 if os.path.islink(self._join(x)):
214 215 return b'l'
215 216 if b'x' in fallback(x):
216 217 return b'x'
217 218 return b''
218 219
219 220 return f
220 221 if self._checkexec:
221 222
222 223 def f(x):
223 224 if b'l' in fallback(x):
224 225 return b'l'
225 226 if util.isexec(self._join(x)):
226 227 return b'x'
227 228 return b''
228 229
229 230 return f
230 231 else:
231 232 return fallback
232 233
233 234 @propertycache
234 235 def _cwd(self):
235 236 # internal config: ui.forcecwd
236 237 forcecwd = self._ui.config(b'ui', b'forcecwd')
237 238 if forcecwd:
238 239 return forcecwd
239 240 return encoding.getcwd()
240 241
241 242 def getcwd(self):
242 243 '''Return the path from which a canonical path is calculated.
243 244
244 245 This path should be used to resolve file patterns or to convert
245 246 canonical paths back to file paths for display. It shouldn't be
246 247 used to get real file paths. Use vfs functions instead.
247 248 '''
248 249 cwd = self._cwd
249 250 if cwd == self._root:
250 251 return b''
251 252 # self._root ends with a path separator if self._root is '/' or 'C:\'
252 253 rootsep = self._root
253 254 if not util.endswithsep(rootsep):
254 255 rootsep += pycompat.ossep
255 256 if cwd.startswith(rootsep):
256 257 return cwd[len(rootsep) :]
257 258 else:
258 259 # we're outside the repo. return an absolute path.
259 260 return cwd
260 261
261 262 def pathto(self, f, cwd=None):
262 263 if cwd is None:
263 264 cwd = self.getcwd()
264 265 path = util.pathto(self._root, cwd, f)
265 266 if self._slash:
266 267 return util.pconvert(path)
267 268 return path
268 269
269 270 def __getitem__(self, key):
270 271 '''Return the current state of key (a filename) in the dirstate.
271 272
272 273 States are:
273 274 n normal
274 275 m needs merging
275 276 r marked for removal
276 277 a marked for addition
277 278 ? not tracked
278 279 '''
279 280 return self._map.get(key, (b"?",))[0]
280 281
281 282 def __contains__(self, key):
282 283 return key in self._map
283 284
284 285 def __iter__(self):
285 286 return iter(sorted(self._map))
286 287
287 288 def items(self):
288 289 return pycompat.iteritems(self._map)
289 290
290 291 iteritems = items
291 292
292 293 def parents(self):
293 294 return [self._validate(p) for p in self._pl]
294 295
295 296 def p1(self):
296 297 return self._validate(self._pl[0])
297 298
298 299 def p2(self):
299 300 return self._validate(self._pl[1])
300 301
301 302 def branch(self):
302 303 return encoding.tolocal(self._branch)
303 304
304 305 def setparents(self, p1, p2=nullid):
305 306 """Set dirstate parents to p1 and p2.
306 307
307 308 When moving from two parents to one, 'm' merged entries a
308 309 adjusted to normal and previous copy records discarded and
309 310 returned by the call.
310 311
311 312 See localrepo.setparents()
312 313 """
313 314 if self._parentwriters == 0:
314 315 raise ValueError(
315 316 b"cannot set dirstate parent outside of "
316 317 b"dirstate.parentchange context manager"
317 318 )
318 319
319 320 self._dirty = True
320 321 oldp2 = self._pl[1]
321 322 if self._origpl is None:
322 323 self._origpl = self._pl
323 324 self._map.setparents(p1, p2)
324 325 copies = {}
325 326 if oldp2 != nullid and p2 == nullid:
326 327 candidatefiles = self._map.nonnormalset.union(
327 328 self._map.otherparentset
328 329 )
329 330 for f in candidatefiles:
330 331 s = self._map.get(f)
331 332 if s is None:
332 333 continue
333 334
334 335 # Discard 'm' markers when moving away from a merge state
335 336 if s[0] == b'm':
336 337 source = self._map.copymap.get(f)
337 338 if source:
338 339 copies[f] = source
339 340 self.normallookup(f)
340 341 # Also fix up otherparent markers
341 342 elif s[0] == b'n' and s[2] == -2:
342 343 source = self._map.copymap.get(f)
343 344 if source:
344 345 copies[f] = source
345 346 self.add(f)
346 347 return copies
347 348
348 349 def setbranch(self, branch):
349 350 self.__class__._branch.set(self, encoding.fromlocal(branch))
350 351 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
351 352 try:
352 353 f.write(self._branch + b'\n')
353 354 f.close()
354 355
355 356 # make sure filecache has the correct stat info for _branch after
356 357 # replacing the underlying file
357 358 ce = self._filecache[b'_branch']
358 359 if ce:
359 360 ce.refresh()
360 361 except: # re-raises
361 362 f.discard()
362 363 raise
363 364
364 365 def invalidate(self):
365 366 '''Causes the next access to reread the dirstate.
366 367
367 368 This is different from localrepo.invalidatedirstate() because it always
368 369 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
369 370 check whether the dirstate has changed before rereading it.'''
370 371
371 372 for a in ("_map", "_branch", "_ignore"):
372 373 if a in self.__dict__:
373 374 delattr(self, a)
374 375 self._lastnormaltime = 0
375 376 self._dirty = False
376 377 self._updatedfiles.clear()
377 378 self._parentwriters = 0
378 379 self._origpl = None
379 380
380 381 def copy(self, source, dest):
381 382 """Mark dest as a copy of source. Unmark dest if source is None."""
382 383 if source == dest:
383 384 return
384 385 self._dirty = True
385 386 if source is not None:
386 387 self._map.copymap[dest] = source
387 388 self._updatedfiles.add(source)
388 389 self._updatedfiles.add(dest)
389 390 elif self._map.copymap.pop(dest, None):
390 391 self._updatedfiles.add(dest)
391 392
392 393 def copied(self, file):
393 394 return self._map.copymap.get(file, None)
394 395
395 396 def copies(self):
396 397 return self._map.copymap
397 398
398 399 def _addpath(self, f, state, mode, size, mtime):
399 400 oldstate = self[f]
400 401 if state == b'a' or oldstate == b'r':
401 402 scmutil.checkfilename(f)
402 403 if self._map.hastrackeddir(f):
403 404 raise error.Abort(
404 405 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
405 406 )
406 407 # shadows
407 408 for d in pathutil.finddirs(f):
408 409 if self._map.hastrackeddir(d):
409 410 break
410 411 entry = self._map.get(d)
411 412 if entry is not None and entry[0] != b'r':
412 413 raise error.Abort(
413 414 _(b'file %r in dirstate clashes with %r')
414 415 % (pycompat.bytestr(d), pycompat.bytestr(f))
415 416 )
416 417 self._dirty = True
417 418 self._updatedfiles.add(f)
418 419 self._map.addfile(f, oldstate, state, mode, size, mtime)
419 420
420 421 def normal(self, f, parentfiledata=None):
421 422 '''Mark a file normal and clean.
422 423
423 424 parentfiledata: (mode, size, mtime) of the clean file
424 425
425 426 parentfiledata should be computed from memory (for mode,
426 427 size), as or close as possible from the point where we
427 428 determined the file was clean, to limit the risk of the
428 429 file having been changed by an external process between the
429 430 moment where the file was determined to be clean and now.'''
430 431 if parentfiledata:
431 432 (mode, size, mtime) = parentfiledata
432 433 else:
433 434 s = os.lstat(self._join(f))
434 435 mode = s.st_mode
435 436 size = s.st_size
436 437 mtime = s[stat.ST_MTIME]
437 438 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
438 439 self._map.copymap.pop(f, None)
439 440 if f in self._map.nonnormalset:
440 441 self._map.nonnormalset.remove(f)
441 442 if mtime > self._lastnormaltime:
442 443 # Remember the most recent modification timeslot for status(),
443 444 # to make sure we won't miss future size-preserving file content
444 445 # modifications that happen within the same timeslot.
445 446 self._lastnormaltime = mtime
446 447
447 448 def normallookup(self, f):
448 449 '''Mark a file normal, but possibly dirty.'''
449 450 if self._pl[1] != nullid:
450 451 # if there is a merge going on and the file was either
451 452 # in state 'm' (-1) or coming from other parent (-2) before
452 453 # being removed, restore that state.
453 454 entry = self._map.get(f)
454 455 if entry is not None:
455 456 if entry[0] == b'r' and entry[2] in (-1, -2):
456 457 source = self._map.copymap.get(f)
457 458 if entry[2] == -1:
458 459 self.merge(f)
459 460 elif entry[2] == -2:
460 461 self.otherparent(f)
461 462 if source:
462 463 self.copy(source, f)
463 464 return
464 465 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
465 466 return
466 467 self._addpath(f, b'n', 0, -1, -1)
467 468 self._map.copymap.pop(f, None)
468 469
469 470 def otherparent(self, f):
470 471 '''Mark as coming from the other parent, always dirty.'''
471 472 if self._pl[1] == nullid:
472 473 raise error.Abort(
473 474 _(b"setting %r to other parent only allowed in merges") % f
474 475 )
475 476 if f in self and self[f] == b'n':
476 477 # merge-like
477 478 self._addpath(f, b'm', 0, -2, -1)
478 479 else:
479 480 # add-like
480 481 self._addpath(f, b'n', 0, -2, -1)
481 482 self._map.copymap.pop(f, None)
482 483
483 484 def add(self, f):
484 485 '''Mark a file added.'''
485 486 self._addpath(f, b'a', 0, -1, -1)
486 487 self._map.copymap.pop(f, None)
487 488
488 489 def remove(self, f):
489 490 '''Mark a file removed.'''
490 491 self._dirty = True
491 492 oldstate = self[f]
492 493 size = 0
493 494 if self._pl[1] != nullid:
494 495 entry = self._map.get(f)
495 496 if entry is not None:
496 497 # backup the previous state
497 498 if entry[0] == b'm': # merge
498 499 size = -1
499 500 elif entry[0] == b'n' and entry[2] == -2: # other parent
500 501 size = -2
501 502 self._map.otherparentset.add(f)
502 503 self._updatedfiles.add(f)
503 504 self._map.removefile(f, oldstate, size)
504 505 if size == 0:
505 506 self._map.copymap.pop(f, None)
506 507
507 508 def merge(self, f):
508 509 '''Mark a file merged.'''
509 510 if self._pl[1] == nullid:
510 511 return self.normallookup(f)
511 512 return self.otherparent(f)
512 513
513 514 def drop(self, f):
514 515 '''Drop a file from the dirstate'''
515 516 oldstate = self[f]
516 517 if self._map.dropfile(f, oldstate):
517 518 self._dirty = True
518 519 self._updatedfiles.add(f)
519 520 self._map.copymap.pop(f, None)
520 521
521 522 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
522 523 if exists is None:
523 524 exists = os.path.lexists(os.path.join(self._root, path))
524 525 if not exists:
525 526 # Maybe a path component exists
526 527 if not ignoremissing and b'/' in path:
527 528 d, f = path.rsplit(b'/', 1)
528 529 d = self._normalize(d, False, ignoremissing, None)
529 530 folded = d + b"/" + f
530 531 else:
531 532 # No path components, preserve original case
532 533 folded = path
533 534 else:
534 535 # recursively normalize leading directory components
535 536 # against dirstate
536 537 if b'/' in normed:
537 538 d, f = normed.rsplit(b'/', 1)
538 539 d = self._normalize(d, False, ignoremissing, True)
539 540 r = self._root + b"/" + d
540 541 folded = d + b"/" + util.fspath(f, r)
541 542 else:
542 543 folded = util.fspath(normed, self._root)
543 544 storemap[normed] = folded
544 545
545 546 return folded
546 547
547 548 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
548 549 normed = util.normcase(path)
549 550 folded = self._map.filefoldmap.get(normed, None)
550 551 if folded is None:
551 552 if isknown:
552 553 folded = path
553 554 else:
554 555 folded = self._discoverpath(
555 556 path, normed, ignoremissing, exists, self._map.filefoldmap
556 557 )
557 558 return folded
558 559
559 560 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
560 561 normed = util.normcase(path)
561 562 folded = self._map.filefoldmap.get(normed, None)
562 563 if folded is None:
563 564 folded = self._map.dirfoldmap.get(normed, None)
564 565 if folded is None:
565 566 if isknown:
566 567 folded = path
567 568 else:
568 569 # store discovered result in dirfoldmap so that future
569 570 # normalizefile calls don't start matching directories
570 571 folded = self._discoverpath(
571 572 path, normed, ignoremissing, exists, self._map.dirfoldmap
572 573 )
573 574 return folded
574 575
575 576 def normalize(self, path, isknown=False, ignoremissing=False):
576 577 '''
577 578 normalize the case of a pathname when on a casefolding filesystem
578 579
579 580 isknown specifies whether the filename came from walking the
580 581 disk, to avoid extra filesystem access.
581 582
582 583 If ignoremissing is True, missing path are returned
583 584 unchanged. Otherwise, we try harder to normalize possibly
584 585 existing path components.
585 586
586 587 The normalized case is determined based on the following precedence:
587 588
588 589 - version of name already stored in the dirstate
589 590 - version of name stored on disk
590 591 - version provided via command arguments
591 592 '''
592 593
593 594 if self._checkcase:
594 595 return self._normalize(path, isknown, ignoremissing)
595 596 return path
596 597
597 598 def clear(self):
598 599 self._map.clear()
599 600 self._lastnormaltime = 0
600 601 self._updatedfiles.clear()
601 602 self._dirty = True
602 603
603 604 def rebuild(self, parent, allfiles, changedfiles=None):
604 605 if changedfiles is None:
605 606 # Rebuild entire dirstate
606 607 to_lookup = allfiles
607 608 to_drop = []
608 609 lastnormaltime = self._lastnormaltime
609 610 self.clear()
610 611 self._lastnormaltime = lastnormaltime
611 612 elif len(changedfiles) < 10:
612 613 # Avoid turning allfiles into a set, which can be expensive if it's
613 614 # large.
614 615 to_lookup = []
615 616 to_drop = []
616 617 for f in changedfiles:
617 618 if f in allfiles:
618 619 to_lookup.append(f)
619 620 else:
620 621 to_drop.append(f)
621 622 else:
622 623 changedfilesset = set(changedfiles)
623 624 to_lookup = changedfilesset & set(allfiles)
624 625 to_drop = changedfilesset - to_lookup
625 626
626 627 if self._origpl is None:
627 628 self._origpl = self._pl
628 629 self._map.setparents(parent, nullid)
629 630
630 631 for f in to_lookup:
631 632 self.normallookup(f)
632 633 for f in to_drop:
633 634 self.drop(f)
634 635
635 636 self._dirty = True
636 637
637 638 def identity(self):
638 639 '''Return identity of dirstate itself to detect changing in storage
639 640
640 641 If identity of previous dirstate is equal to this, writing
641 642 changes based on the former dirstate out can keep consistency.
642 643 '''
643 644 return self._map.identity
644 645
645 646 def write(self, tr):
646 647 if not self._dirty:
647 648 return
648 649
649 650 filename = self._filename
650 651 if tr:
651 652 # 'dirstate.write()' is not only for writing in-memory
652 653 # changes out, but also for dropping ambiguous timestamp.
653 654 # delayed writing re-raise "ambiguous timestamp issue".
654 655 # See also the wiki page below for detail:
655 656 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
656 657
657 658 # emulate dropping timestamp in 'parsers.pack_dirstate'
658 659 now = _getfsnow(self._opener)
659 660 self._map.clearambiguoustimes(self._updatedfiles, now)
660 661
661 662 # emulate that all 'dirstate.normal' results are written out
662 663 self._lastnormaltime = 0
663 664 self._updatedfiles.clear()
664 665
665 666 # delay writing in-memory changes out
666 667 tr.addfilegenerator(
667 668 b'dirstate',
668 669 (self._filename,),
669 670 self._writedirstate,
670 671 location=b'plain',
671 672 )
672 673 return
673 674
674 675 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
675 676 self._writedirstate(st)
676 677
677 678 def addparentchangecallback(self, category, callback):
678 679 """add a callback to be called when the wd parents are changed
679 680
680 681 Callback will be called with the following arguments:
681 682 dirstate, (oldp1, oldp2), (newp1, newp2)
682 683
683 684 Category is a unique identifier to allow overwriting an old callback
684 685 with a newer callback.
685 686 """
686 687 self._plchangecallbacks[category] = callback
687 688
688 689 def _writedirstate(self, st):
689 690 # notify callbacks about parents change
690 691 if self._origpl is not None and self._origpl != self._pl:
691 692 for c, callback in sorted(
692 693 pycompat.iteritems(self._plchangecallbacks)
693 694 ):
694 695 callback(self, self._origpl, self._pl)
695 696 self._origpl = None
696 697 # use the modification time of the newly created temporary file as the
697 698 # filesystem's notion of 'now'
698 699 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
699 700
700 701 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
701 702 # timestamp of each entries in dirstate, because of 'now > mtime'
702 703 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
703 704 if delaywrite > 0:
704 705 # do we have any files to delay for?
705 706 for f, e in pycompat.iteritems(self._map):
706 707 if e[0] == b'n' and e[3] == now:
707 708 import time # to avoid useless import
708 709
709 710 # rather than sleep n seconds, sleep until the next
710 711 # multiple of n seconds
711 712 clock = time.time()
712 713 start = int(clock) - (int(clock) % delaywrite)
713 714 end = start + delaywrite
714 715 time.sleep(end - clock)
715 716 now = end # trust our estimate that the end is near now
716 717 break
717 718
718 719 self._map.write(st, now)
719 720 self._lastnormaltime = 0
720 721 self._dirty = False
721 722
722 723 def _dirignore(self, f):
723 724 if self._ignore(f):
724 725 return True
725 726 for p in pathutil.finddirs(f):
726 727 if self._ignore(p):
727 728 return True
728 729 return False
729 730
730 731 def _ignorefiles(self):
731 732 files = []
732 733 if os.path.exists(self._join(b'.hgignore')):
733 734 files.append(self._join(b'.hgignore'))
734 735 for name, path in self._ui.configitems(b"ui"):
735 736 if name == b'ignore' or name.startswith(b'ignore.'):
736 737 # we need to use os.path.join here rather than self._join
737 738 # because path is arbitrary and user-specified
738 739 files.append(os.path.join(self._rootdir, util.expandpath(path)))
739 740 return files
740 741
741 742 def _ignorefileandline(self, f):
742 743 files = collections.deque(self._ignorefiles())
743 744 visited = set()
744 745 while files:
745 746 i = files.popleft()
746 747 patterns = matchmod.readpatternfile(
747 748 i, self._ui.warn, sourceinfo=True
748 749 )
749 750 for pattern, lineno, line in patterns:
750 751 kind, p = matchmod._patsplit(pattern, b'glob')
751 752 if kind == b"subinclude":
752 753 if p not in visited:
753 754 files.append(p)
754 755 continue
755 756 m = matchmod.match(
756 757 self._root, b'', [], [pattern], warn=self._ui.warn
757 758 )
758 759 if m(f):
759 760 return (i, lineno, line)
760 761 visited.add(i)
761 762 return (None, -1, b"")
762 763
763 764 def _walkexplicit(self, match, subrepos):
764 765 '''Get stat data about the files explicitly specified by match.
765 766
766 767 Return a triple (results, dirsfound, dirsnotfound).
767 768 - results is a mapping from filename to stat result. It also contains
768 769 listings mapping subrepos and .hg to None.
769 770 - dirsfound is a list of files found to be directories.
770 771 - dirsnotfound is a list of files that the dirstate thinks are
771 772 directories and that were not found.'''
772 773
773 774 def badtype(mode):
774 775 kind = _(b'unknown')
775 776 if stat.S_ISCHR(mode):
776 777 kind = _(b'character device')
777 778 elif stat.S_ISBLK(mode):
778 779 kind = _(b'block device')
779 780 elif stat.S_ISFIFO(mode):
780 781 kind = _(b'fifo')
781 782 elif stat.S_ISSOCK(mode):
782 783 kind = _(b'socket')
783 784 elif stat.S_ISDIR(mode):
784 785 kind = _(b'directory')
785 786 return _(b'unsupported file type (type is %s)') % kind
786 787
787 788 badfn = match.bad
788 789 dmap = self._map
789 790 lstat = os.lstat
790 791 getkind = stat.S_IFMT
791 792 dirkind = stat.S_IFDIR
792 793 regkind = stat.S_IFREG
793 794 lnkkind = stat.S_IFLNK
794 795 join = self._join
795 796 dirsfound = []
796 797 foundadd = dirsfound.append
797 798 dirsnotfound = []
798 799 notfoundadd = dirsnotfound.append
799 800
800 801 if not match.isexact() and self._checkcase:
801 802 normalize = self._normalize
802 803 else:
803 804 normalize = None
804 805
805 806 files = sorted(match.files())
806 807 subrepos.sort()
807 808 i, j = 0, 0
808 809 while i < len(files) and j < len(subrepos):
809 810 subpath = subrepos[j] + b"/"
810 811 if files[i] < subpath:
811 812 i += 1
812 813 continue
813 814 while i < len(files) and files[i].startswith(subpath):
814 815 del files[i]
815 816 j += 1
816 817
817 818 if not files or b'' in files:
818 819 files = [b'']
819 820 # constructing the foldmap is expensive, so don't do it for the
820 821 # common case where files is ['']
821 822 normalize = None
822 823 results = dict.fromkeys(subrepos)
823 824 results[b'.hg'] = None
824 825
825 826 for ff in files:
826 827 if normalize:
827 828 nf = normalize(ff, False, True)
828 829 else:
829 830 nf = ff
830 831 if nf in results:
831 832 continue
832 833
833 834 try:
834 835 st = lstat(join(nf))
835 836 kind = getkind(st.st_mode)
836 837 if kind == dirkind:
837 838 if nf in dmap:
838 839 # file replaced by dir on disk but still in dirstate
839 840 results[nf] = None
840 841 foundadd((nf, ff))
841 842 elif kind == regkind or kind == lnkkind:
842 843 results[nf] = st
843 844 else:
844 845 badfn(ff, badtype(kind))
845 846 if nf in dmap:
846 847 results[nf] = None
847 848 except OSError as inst: # nf not found on disk - it is dirstate only
848 849 if nf in dmap: # does it exactly match a missing file?
849 850 results[nf] = None
850 851 else: # does it match a missing directory?
851 852 if self._map.hasdir(nf):
852 853 notfoundadd(nf)
853 854 else:
854 855 badfn(ff, encoding.strtolocal(inst.strerror))
855 856
856 857 # match.files() may contain explicitly-specified paths that shouldn't
857 858 # be taken; drop them from the list of files found. dirsfound/notfound
858 859 # aren't filtered here because they will be tested later.
859 860 if match.anypats():
860 861 for f in list(results):
861 862 if f == b'.hg' or f in subrepos:
862 863 # keep sentinel to disable further out-of-repo walks
863 864 continue
864 865 if not match(f):
865 866 del results[f]
866 867
867 868 # Case insensitive filesystems cannot rely on lstat() failing to detect
868 869 # a case-only rename. Prune the stat object for any file that does not
869 870 # match the case in the filesystem, if there are multiple files that
870 871 # normalize to the same path.
871 872 if match.isexact() and self._checkcase:
872 873 normed = {}
873 874
874 875 for f, st in pycompat.iteritems(results):
875 876 if st is None:
876 877 continue
877 878
878 879 nc = util.normcase(f)
879 880 paths = normed.get(nc)
880 881
881 882 if paths is None:
882 883 paths = set()
883 884 normed[nc] = paths
884 885
885 886 paths.add(f)
886 887
887 888 for norm, paths in pycompat.iteritems(normed):
888 889 if len(paths) > 1:
889 890 for path in paths:
890 891 folded = self._discoverpath(
891 892 path, norm, True, None, self._map.dirfoldmap
892 893 )
893 894 if path != folded:
894 895 results[path] = None
895 896
896 897 return results, dirsfound, dirsnotfound
897 898
898 899 def walk(self, match, subrepos, unknown, ignored, full=True):
899 900 '''
900 901 Walk recursively through the directory tree, finding all files
901 902 matched by match.
902 903
903 904 If full is False, maybe skip some known-clean files.
904 905
905 906 Return a dict mapping filename to stat-like object (either
906 907 mercurial.osutil.stat instance or return value of os.stat()).
907 908
908 909 '''
909 910 # full is a flag that extensions that hook into walk can use -- this
910 911 # implementation doesn't use it at all. This satisfies the contract
911 912 # because we only guarantee a "maybe".
912 913
913 914 if ignored:
914 915 ignore = util.never
915 916 dirignore = util.never
916 917 elif unknown:
917 918 ignore = self._ignore
918 919 dirignore = self._dirignore
919 920 else:
920 921 # if not unknown and not ignored, drop dir recursion and step 2
921 922 ignore = util.always
922 923 dirignore = util.always
923 924
924 925 matchfn = match.matchfn
925 926 matchalways = match.always()
926 927 matchtdir = match.traversedir
927 928 dmap = self._map
928 929 listdir = util.listdir
929 930 lstat = os.lstat
930 931 dirkind = stat.S_IFDIR
931 932 regkind = stat.S_IFREG
932 933 lnkkind = stat.S_IFLNK
933 934 join = self._join
934 935
935 936 exact = skipstep3 = False
936 937 if match.isexact(): # match.exact
937 938 exact = True
938 939 dirignore = util.always # skip step 2
939 940 elif match.prefix(): # match.match, no patterns
940 941 skipstep3 = True
941 942
942 943 if not exact and self._checkcase:
943 944 normalize = self._normalize
944 945 normalizefile = self._normalizefile
945 946 skipstep3 = False
946 947 else:
947 948 normalize = self._normalize
948 949 normalizefile = None
949 950
950 951 # step 1: find all explicit files
951 952 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
952 953 if matchtdir:
953 954 for d in work:
954 955 matchtdir(d[0])
955 956 for d in dirsnotfound:
956 957 matchtdir(d)
957 958
958 959 skipstep3 = skipstep3 and not (work or dirsnotfound)
959 960 work = [d for d in work if not dirignore(d[0])]
960 961
961 962 # step 2: visit subdirectories
962 963 def traverse(work, alreadynormed):
963 964 wadd = work.append
964 965 while work:
965 966 tracing.counter('dirstate.walk work', len(work))
966 967 nd = work.pop()
967 968 visitentries = match.visitchildrenset(nd)
968 969 if not visitentries:
969 970 continue
970 971 if visitentries == b'this' or visitentries == b'all':
971 972 visitentries = None
972 973 skip = None
973 974 if nd != b'':
974 975 skip = b'.hg'
975 976 try:
976 977 with tracing.log('dirstate.walk.traverse listdir %s', nd):
977 978 entries = listdir(join(nd), stat=True, skip=skip)
978 979 except OSError as inst:
979 980 if inst.errno in (errno.EACCES, errno.ENOENT):
980 981 match.bad(
981 982 self.pathto(nd), encoding.strtolocal(inst.strerror)
982 983 )
983 984 continue
984 985 raise
985 986 for f, kind, st in entries:
986 987 # Some matchers may return files in the visitentries set,
987 988 # instead of 'this', if the matcher explicitly mentions them
988 989 # and is not an exactmatcher. This is acceptable; we do not
989 990 # make any hard assumptions about file-or-directory below
990 991 # based on the presence of `f` in visitentries. If
991 992 # visitchildrenset returned a set, we can always skip the
992 993 # entries *not* in the set it provided regardless of whether
993 994 # they're actually a file or a directory.
994 995 if visitentries and f not in visitentries:
995 996 continue
996 997 if normalizefile:
997 998 # even though f might be a directory, we're only
998 999 # interested in comparing it to files currently in the
999 1000 # dmap -- therefore normalizefile is enough
1000 1001 nf = normalizefile(
1001 1002 nd and (nd + b"/" + f) or f, True, True
1002 1003 )
1003 1004 else:
1004 1005 nf = nd and (nd + b"/" + f) or f
1005 1006 if nf not in results:
1006 1007 if kind == dirkind:
1007 1008 if not ignore(nf):
1008 1009 if matchtdir:
1009 1010 matchtdir(nf)
1010 1011 wadd(nf)
1011 1012 if nf in dmap and (matchalways or matchfn(nf)):
1012 1013 results[nf] = None
1013 1014 elif kind == regkind or kind == lnkkind:
1014 1015 if nf in dmap:
1015 1016 if matchalways or matchfn(nf):
1016 1017 results[nf] = st
1017 1018 elif (matchalways or matchfn(nf)) and not ignore(
1018 1019 nf
1019 1020 ):
1020 1021 # unknown file -- normalize if necessary
1021 1022 if not alreadynormed:
1022 1023 nf = normalize(nf, False, True)
1023 1024 results[nf] = st
1024 1025 elif nf in dmap and (matchalways or matchfn(nf)):
1025 1026 results[nf] = None
1026 1027
1027 1028 for nd, d in work:
1028 1029 # alreadynormed means that processwork doesn't have to do any
1029 1030 # expensive directory normalization
1030 1031 alreadynormed = not normalize or nd == d
1031 1032 traverse([d], alreadynormed)
1032 1033
1033 1034 for s in subrepos:
1034 1035 del results[s]
1035 1036 del results[b'.hg']
1036 1037
1037 1038 # step 3: visit remaining files from dmap
1038 1039 if not skipstep3 and not exact:
1039 1040 # If a dmap file is not in results yet, it was either
1040 1041 # a) not matching matchfn b) ignored, c) missing, or d) under a
1041 1042 # symlink directory.
1042 1043 if not results and matchalways:
1043 1044 visit = [f for f in dmap]
1044 1045 else:
1045 1046 visit = [f for f in dmap if f not in results and matchfn(f)]
1046 1047 visit.sort()
1047 1048
1048 1049 if unknown:
1049 1050 # unknown == True means we walked all dirs under the roots
1050 1051 # that wasn't ignored, and everything that matched was stat'ed
1051 1052 # and is already in results.
1052 1053 # The rest must thus be ignored or under a symlink.
1053 1054 audit_path = pathutil.pathauditor(self._root, cached=True)
1054 1055
1055 1056 for nf in iter(visit):
1056 1057 # If a stat for the same file was already added with a
1057 1058 # different case, don't add one for this, since that would
1058 1059 # make it appear as if the file exists under both names
1059 1060 # on disk.
1060 1061 if (
1061 1062 normalizefile
1062 1063 and normalizefile(nf, True, True) in results
1063 1064 ):
1064 1065 results[nf] = None
1065 1066 # Report ignored items in the dmap as long as they are not
1066 1067 # under a symlink directory.
1067 1068 elif audit_path.check(nf):
1068 1069 try:
1069 1070 results[nf] = lstat(join(nf))
1070 1071 # file was just ignored, no links, and exists
1071 1072 except OSError:
1072 1073 # file doesn't exist
1073 1074 results[nf] = None
1074 1075 else:
1075 1076 # It's either missing or under a symlink directory
1076 1077 # which we in this case report as missing
1077 1078 results[nf] = None
1078 1079 else:
1079 1080 # We may not have walked the full directory tree above,
1080 1081 # so stat and check everything we missed.
1081 1082 iv = iter(visit)
1082 1083 for st in util.statfiles([join(i) for i in visit]):
1083 1084 results[next(iv)] = st
1084 1085 return results
1085 1086
1086 def _rust_status(self, matcher, list_clean):
1087 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1087 1088 # Force Rayon (Rust parallelism library) to respect the number of
1088 1089 # workers. This is a temporary workaround until Rust code knows
1089 1090 # how to read the config file.
1090 1091 numcpus = self._ui.configint(b"worker", b"numcpus")
1091 1092 if numcpus is not None:
1092 1093 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1093 1094
1094 1095 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1095 1096 if not workers_enabled:
1096 1097 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1097 1098
1098 1099 (
1099 1100 lookup,
1100 1101 modified,
1101 1102 added,
1102 1103 removed,
1103 1104 deleted,
1105 clean,
1106 ignored,
1104 1107 unknown,
1105 clean,
1108 warnings,
1109 bad,
1106 1110 ) = rustmod.status(
1107 1111 self._map._rustmap,
1108 1112 matcher,
1109 1113 self._rootdir,
1110 bool(list_clean),
1114 self._ignorefiles(),
1115 self._checkexec,
1111 1116 self._lastnormaltime,
1112 self._checkexec,
1117 bool(list_clean),
1118 bool(list_ignored),
1119 bool(list_unknown),
1113 1120 )
1121 if self._ui.warn:
1122 for item in warnings:
1123 if isinstance(item, tuple):
1124 file_path, syntax = item
1125 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1126 file_path,
1127 syntax,
1128 )
1129 self._ui.warn(msg)
1130 else:
1131 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1132 self._ui.warn(
1133 msg
1134 % (
1135 pathutil.canonpath(
1136 self._rootdir, self._rootdir, item
1137 ),
1138 b"No such file or directory",
1139 )
1140 )
1141
1142 for (fn, message) in bad:
1143 matcher.bad(fn, encoding.strtolocal(message))
1114 1144
1115 1145 status = scmutil.status(
1116 1146 modified=modified,
1117 1147 added=added,
1118 1148 removed=removed,
1119 1149 deleted=deleted,
1120 1150 unknown=unknown,
1121 ignored=[],
1151 ignored=ignored,
1122 1152 clean=clean,
1123 1153 )
1124 1154 return (lookup, status)
1125 1155
1126 1156 def status(self, match, subrepos, ignored, clean, unknown):
1127 1157 '''Determine the status of the working copy relative to the
1128 1158 dirstate and return a pair of (unsure, status), where status is of type
1129 1159 scmutil.status and:
1130 1160
1131 1161 unsure:
1132 1162 files that might have been modified since the dirstate was
1133 1163 written, but need to be read to be sure (size is the same
1134 1164 but mtime differs)
1135 1165 status.modified:
1136 1166 files that have definitely been modified since the dirstate
1137 1167 was written (different size or mode)
1138 1168 status.clean:
1139 1169 files that have definitely not been modified since the
1140 1170 dirstate was written
1141 1171 '''
1142 1172 listignored, listclean, listunknown = ignored, clean, unknown
1143 1173 lookup, modified, added, unknown, ignored = [], [], [], [], []
1144 1174 removed, deleted, clean = [], [], []
1145 1175
1146 1176 dmap = self._map
1147 1177 dmap.preload()
1148 1178
1149 1179 use_rust = True
1150 1180
1151 allowed_matchers = (matchmod.alwaysmatcher, matchmod.exactmatcher)
1181 allowed_matchers = (
1182 matchmod.alwaysmatcher,
1183 matchmod.exactmatcher,
1184 matchmod.includematcher,
1185 )
1152 1186
1153 1187 if rustmod is None:
1154 1188 use_rust = False
1189 elif self._checkcase:
1190 # Case-insensitive filesystems are not handled yet
1191 use_rust = False
1155 1192 elif subrepos:
1156 1193 use_rust = False
1157 elif bool(listunknown):
1158 # Pathauditor does not exist yet in Rust, unknown files
1159 # can't be trusted.
1194 elif sparse.enabled:
1160 1195 use_rust = False
1161 elif self._ignorefiles() and listignored:
1162 # Rust has no ignore mechanism yet, so don't use Rust for
1163 # commands that need ignore.
1196 elif match.traversedir is not None:
1164 1197 use_rust = False
1165 1198 elif not isinstance(match, allowed_matchers):
1166 1199 # Matchers have yet to be implemented
1167 1200 use_rust = False
1168 1201
1169 1202 if use_rust:
1170 return self._rust_status(match, listclean)
1203 try:
1204 return self._rust_status(
1205 match, listclean, listignored, listunknown
1206 )
1207 except rustmod.FallbackError:
1208 pass
1171 1209
1172 1210 def noop(f):
1173 1211 pass
1174 1212
1175 1213 dcontains = dmap.__contains__
1176 1214 dget = dmap.__getitem__
1177 1215 ladd = lookup.append # aka "unsure"
1178 1216 madd = modified.append
1179 1217 aadd = added.append
1180 1218 uadd = unknown.append if listunknown else noop
1181 1219 iadd = ignored.append if listignored else noop
1182 1220 radd = removed.append
1183 1221 dadd = deleted.append
1184 1222 cadd = clean.append if listclean else noop
1185 1223 mexact = match.exact
1186 1224 dirignore = self._dirignore
1187 1225 checkexec = self._checkexec
1188 1226 copymap = self._map.copymap
1189 1227 lastnormaltime = self._lastnormaltime
1190 1228
1191 1229 # We need to do full walks when either
1192 1230 # - we're listing all clean files, or
1193 1231 # - match.traversedir does something, because match.traversedir should
1194 1232 # be called for every dir in the working dir
1195 1233 full = listclean or match.traversedir is not None
1196 1234 for fn, st in pycompat.iteritems(
1197 1235 self.walk(match, subrepos, listunknown, listignored, full=full)
1198 1236 ):
1199 1237 if not dcontains(fn):
1200 1238 if (listignored or mexact(fn)) and dirignore(fn):
1201 1239 if listignored:
1202 1240 iadd(fn)
1203 1241 else:
1204 1242 uadd(fn)
1205 1243 continue
1206 1244
1207 1245 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1208 1246 # written like that for performance reasons. dmap[fn] is not a
1209 1247 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1210 1248 # opcode has fast paths when the value to be unpacked is a tuple or
1211 1249 # a list, but falls back to creating a full-fledged iterator in
1212 1250 # general. That is much slower than simply accessing and storing the
1213 1251 # tuple members one by one.
1214 1252 t = dget(fn)
1215 1253 state = t[0]
1216 1254 mode = t[1]
1217 1255 size = t[2]
1218 1256 time = t[3]
1219 1257
1220 1258 if not st and state in b"nma":
1221 1259 dadd(fn)
1222 1260 elif state == b'n':
1223 1261 if (
1224 1262 size >= 0
1225 1263 and (
1226 1264 (size != st.st_size and size != st.st_size & _rangemask)
1227 1265 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1228 1266 )
1229 1267 or size == -2 # other parent
1230 1268 or fn in copymap
1231 1269 ):
1232 1270 madd(fn)
1233 1271 elif (
1234 1272 time != st[stat.ST_MTIME]
1235 1273 and time != st[stat.ST_MTIME] & _rangemask
1236 1274 ):
1237 1275 ladd(fn)
1238 1276 elif st[stat.ST_MTIME] == lastnormaltime:
1239 1277 # fn may have just been marked as normal and it may have
1240 1278 # changed in the same second without changing its size.
1241 1279 # This can happen if we quickly do multiple commits.
1242 1280 # Force lookup, so we don't miss such a racy file change.
1243 1281 ladd(fn)
1244 1282 elif listclean:
1245 1283 cadd(fn)
1246 1284 elif state == b'm':
1247 1285 madd(fn)
1248 1286 elif state == b'a':
1249 1287 aadd(fn)
1250 1288 elif state == b'r':
1251 1289 radd(fn)
1252
1253 return (
1254 lookup,
1255 scmutil.status(
1256 modified, added, removed, deleted, unknown, ignored, clean
1257 ),
1290 status = scmutil.status(
1291 modified, added, removed, deleted, unknown, ignored, clean
1258 1292 )
1293 return (lookup, status)
1259 1294
1260 1295 def matches(self, match):
1261 1296 '''
1262 1297 return files in the dirstate (in whatever state) filtered by match
1263 1298 '''
1264 1299 dmap = self._map
1265 1300 if rustmod is not None:
1266 1301 dmap = self._map._rustmap
1267 1302
1268 1303 if match.always():
1269 1304 return dmap.keys()
1270 1305 files = match.files()
1271 1306 if match.isexact():
1272 1307 # fast path -- filter the other way around, since typically files is
1273 1308 # much smaller than dmap
1274 1309 return [f for f in files if f in dmap]
1275 1310 if match.prefix() and all(fn in dmap for fn in files):
1276 1311 # fast path -- all the values are known to be files, so just return
1277 1312 # that
1278 1313 return list(files)
1279 1314 return [f for f in dmap if match(f)]
1280 1315
1281 1316 def _actualfilename(self, tr):
1282 1317 if tr:
1283 1318 return self._pendingfilename
1284 1319 else:
1285 1320 return self._filename
1286 1321
1287 1322 def savebackup(self, tr, backupname):
1288 1323 '''Save current dirstate into backup file'''
1289 1324 filename = self._actualfilename(tr)
1290 1325 assert backupname != filename
1291 1326
1292 1327 # use '_writedirstate' instead of 'write' to write changes certainly,
1293 1328 # because the latter omits writing out if transaction is running.
1294 1329 # output file will be used to create backup of dirstate at this point.
1295 1330 if self._dirty or not self._opener.exists(filename):
1296 1331 self._writedirstate(
1297 1332 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1298 1333 )
1299 1334
1300 1335 if tr:
1301 1336 # ensure that subsequent tr.writepending returns True for
1302 1337 # changes written out above, even if dirstate is never
1303 1338 # changed after this
1304 1339 tr.addfilegenerator(
1305 1340 b'dirstate',
1306 1341 (self._filename,),
1307 1342 self._writedirstate,
1308 1343 location=b'plain',
1309 1344 )
1310 1345
1311 1346 # ensure that pending file written above is unlinked at
1312 1347 # failure, even if tr.writepending isn't invoked until the
1313 1348 # end of this transaction
1314 1349 tr.registertmp(filename, location=b'plain')
1315 1350
1316 1351 self._opener.tryunlink(backupname)
1317 1352 # hardlink backup is okay because _writedirstate is always called
1318 1353 # with an "atomictemp=True" file.
1319 1354 util.copyfile(
1320 1355 self._opener.join(filename),
1321 1356 self._opener.join(backupname),
1322 1357 hardlink=True,
1323 1358 )
1324 1359
1325 1360 def restorebackup(self, tr, backupname):
1326 1361 '''Restore dirstate by backup file'''
1327 1362 # this "invalidate()" prevents "wlock.release()" from writing
1328 1363 # changes of dirstate out after restoring from backup file
1329 1364 self.invalidate()
1330 1365 filename = self._actualfilename(tr)
1331 1366 o = self._opener
1332 1367 if util.samefile(o.join(backupname), o.join(filename)):
1333 1368 o.unlink(backupname)
1334 1369 else:
1335 1370 o.rename(backupname, filename, checkambig=True)
1336 1371
1337 1372 def clearbackup(self, tr, backupname):
1338 1373 '''Clear backup file'''
1339 1374 self._opener.unlink(backupname)
1340 1375
1341 1376
1342 1377 class dirstatemap(object):
1343 1378 """Map encapsulating the dirstate's contents.
1344 1379
1345 1380 The dirstate contains the following state:
1346 1381
1347 1382 - `identity` is the identity of the dirstate file, which can be used to
1348 1383 detect when changes have occurred to the dirstate file.
1349 1384
1350 1385 - `parents` is a pair containing the parents of the working copy. The
1351 1386 parents are updated by calling `setparents`.
1352 1387
1353 1388 - the state map maps filenames to tuples of (state, mode, size, mtime),
1354 1389 where state is a single character representing 'normal', 'added',
1355 1390 'removed', or 'merged'. It is read by treating the dirstate as a
1356 1391 dict. File state is updated by calling the `addfile`, `removefile` and
1357 1392 `dropfile` methods.
1358 1393
1359 1394 - `copymap` maps destination filenames to their source filename.
1360 1395
1361 1396 The dirstate also provides the following views onto the state:
1362 1397
1363 1398 - `nonnormalset` is a set of the filenames that have state other
1364 1399 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1365 1400
1366 1401 - `otherparentset` is a set of the filenames that are marked as coming
1367 1402 from the second parent when the dirstate is currently being merged.
1368 1403
1369 1404 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1370 1405 form that they appear as in the dirstate.
1371 1406
1372 1407 - `dirfoldmap` is a dict mapping normalized directory names to the
1373 1408 denormalized form that they appear as in the dirstate.
1374 1409 """
1375 1410
1376 1411 def __init__(self, ui, opener, root):
1377 1412 self._ui = ui
1378 1413 self._opener = opener
1379 1414 self._root = root
1380 1415 self._filename = b'dirstate'
1381 1416
1382 1417 self._parents = None
1383 1418 self._dirtyparents = False
1384 1419
1385 1420 # for consistent view between _pl() and _read() invocations
1386 1421 self._pendingmode = None
1387 1422
1388 1423 @propertycache
1389 1424 def _map(self):
1390 1425 self._map = {}
1391 1426 self.read()
1392 1427 return self._map
1393 1428
1394 1429 @propertycache
1395 1430 def copymap(self):
1396 1431 self.copymap = {}
1397 1432 self._map
1398 1433 return self.copymap
1399 1434
1400 1435 def clear(self):
1401 1436 self._map.clear()
1402 1437 self.copymap.clear()
1403 1438 self.setparents(nullid, nullid)
1404 1439 util.clearcachedproperty(self, b"_dirs")
1405 1440 util.clearcachedproperty(self, b"_alldirs")
1406 1441 util.clearcachedproperty(self, b"filefoldmap")
1407 1442 util.clearcachedproperty(self, b"dirfoldmap")
1408 1443 util.clearcachedproperty(self, b"nonnormalset")
1409 1444 util.clearcachedproperty(self, b"otherparentset")
1410 1445
1411 1446 def items(self):
1412 1447 return pycompat.iteritems(self._map)
1413 1448
1414 1449 # forward for python2,3 compat
1415 1450 iteritems = items
1416 1451
1417 1452 def __len__(self):
1418 1453 return len(self._map)
1419 1454
1420 1455 def __iter__(self):
1421 1456 return iter(self._map)
1422 1457
1423 1458 def get(self, key, default=None):
1424 1459 return self._map.get(key, default)
1425 1460
1426 1461 def __contains__(self, key):
1427 1462 return key in self._map
1428 1463
1429 1464 def __getitem__(self, key):
1430 1465 return self._map[key]
1431 1466
1432 1467 def keys(self):
1433 1468 return self._map.keys()
1434 1469
1435 1470 def preload(self):
1436 1471 """Loads the underlying data, if it's not already loaded"""
1437 1472 self._map
1438 1473
1439 1474 def addfile(self, f, oldstate, state, mode, size, mtime):
1440 1475 """Add a tracked file to the dirstate."""
1441 1476 if oldstate in b"?r" and "_dirs" in self.__dict__:
1442 1477 self._dirs.addpath(f)
1443 1478 if oldstate == b"?" and "_alldirs" in self.__dict__:
1444 1479 self._alldirs.addpath(f)
1445 1480 self._map[f] = dirstatetuple(state, mode, size, mtime)
1446 1481 if state != b'n' or mtime == -1:
1447 1482 self.nonnormalset.add(f)
1448 1483 if size == -2:
1449 1484 self.otherparentset.add(f)
1450 1485
1451 1486 def removefile(self, f, oldstate, size):
1452 1487 """
1453 1488 Mark a file as removed in the dirstate.
1454 1489
1455 1490 The `size` parameter is used to store sentinel values that indicate
1456 1491 the file's previous state. In the future, we should refactor this
1457 1492 to be more explicit about what that state is.
1458 1493 """
1459 1494 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1460 1495 self._dirs.delpath(f)
1461 1496 if oldstate == b"?" and "_alldirs" in self.__dict__:
1462 1497 self._alldirs.addpath(f)
1463 1498 if "filefoldmap" in self.__dict__:
1464 1499 normed = util.normcase(f)
1465 1500 self.filefoldmap.pop(normed, None)
1466 1501 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1467 1502 self.nonnormalset.add(f)
1468 1503
1469 1504 def dropfile(self, f, oldstate):
1470 1505 """
1471 1506 Remove a file from the dirstate. Returns True if the file was
1472 1507 previously recorded.
1473 1508 """
1474 1509 exists = self._map.pop(f, None) is not None
1475 1510 if exists:
1476 1511 if oldstate != b"r" and "_dirs" in self.__dict__:
1477 1512 self._dirs.delpath(f)
1478 1513 if "_alldirs" in self.__dict__:
1479 1514 self._alldirs.delpath(f)
1480 1515 if "filefoldmap" in self.__dict__:
1481 1516 normed = util.normcase(f)
1482 1517 self.filefoldmap.pop(normed, None)
1483 1518 self.nonnormalset.discard(f)
1484 1519 return exists
1485 1520
1486 1521 def clearambiguoustimes(self, files, now):
1487 1522 for f in files:
1488 1523 e = self.get(f)
1489 1524 if e is not None and e[0] == b'n' and e[3] == now:
1490 1525 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1491 1526 self.nonnormalset.add(f)
1492 1527
1493 1528 def nonnormalentries(self):
1494 1529 '''Compute the nonnormal dirstate entries from the dmap'''
1495 1530 try:
1496 1531 return parsers.nonnormalotherparententries(self._map)
1497 1532 except AttributeError:
1498 1533 nonnorm = set()
1499 1534 otherparent = set()
1500 1535 for fname, e in pycompat.iteritems(self._map):
1501 1536 if e[0] != b'n' or e[3] == -1:
1502 1537 nonnorm.add(fname)
1503 1538 if e[0] == b'n' and e[2] == -2:
1504 1539 otherparent.add(fname)
1505 1540 return nonnorm, otherparent
1506 1541
1507 1542 @propertycache
1508 1543 def filefoldmap(self):
1509 1544 """Returns a dictionary mapping normalized case paths to their
1510 1545 non-normalized versions.
1511 1546 """
1512 1547 try:
1513 1548 makefilefoldmap = parsers.make_file_foldmap
1514 1549 except AttributeError:
1515 1550 pass
1516 1551 else:
1517 1552 return makefilefoldmap(
1518 1553 self._map, util.normcasespec, util.normcasefallback
1519 1554 )
1520 1555
1521 1556 f = {}
1522 1557 normcase = util.normcase
1523 1558 for name, s in pycompat.iteritems(self._map):
1524 1559 if s[0] != b'r':
1525 1560 f[normcase(name)] = name
1526 1561 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1527 1562 return f
1528 1563
1529 1564 def hastrackeddir(self, d):
1530 1565 """
1531 1566 Returns True if the dirstate contains a tracked (not removed) file
1532 1567 in this directory.
1533 1568 """
1534 1569 return d in self._dirs
1535 1570
1536 1571 def hasdir(self, d):
1537 1572 """
1538 1573 Returns True if the dirstate contains a file (tracked or removed)
1539 1574 in this directory.
1540 1575 """
1541 1576 return d in self._alldirs
1542 1577
1543 1578 @propertycache
1544 1579 def _dirs(self):
1545 1580 return pathutil.dirs(self._map, b'r')
1546 1581
1547 1582 @propertycache
1548 1583 def _alldirs(self):
1549 1584 return pathutil.dirs(self._map)
1550 1585
1551 1586 def _opendirstatefile(self):
1552 1587 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1553 1588 if self._pendingmode is not None and self._pendingmode != mode:
1554 1589 fp.close()
1555 1590 raise error.Abort(
1556 1591 _(b'working directory state may be changed parallelly')
1557 1592 )
1558 1593 self._pendingmode = mode
1559 1594 return fp
1560 1595
1561 1596 def parents(self):
1562 1597 if not self._parents:
1563 1598 try:
1564 1599 fp = self._opendirstatefile()
1565 1600 st = fp.read(40)
1566 1601 fp.close()
1567 1602 except IOError as err:
1568 1603 if err.errno != errno.ENOENT:
1569 1604 raise
1570 1605 # File doesn't exist, so the current state is empty
1571 1606 st = b''
1572 1607
1573 1608 l = len(st)
1574 1609 if l == 40:
1575 1610 self._parents = (st[:20], st[20:40])
1576 1611 elif l == 0:
1577 1612 self._parents = (nullid, nullid)
1578 1613 else:
1579 1614 raise error.Abort(
1580 1615 _(b'working directory state appears damaged!')
1581 1616 )
1582 1617
1583 1618 return self._parents
1584 1619
1585 1620 def setparents(self, p1, p2):
1586 1621 self._parents = (p1, p2)
1587 1622 self._dirtyparents = True
1588 1623
1589 1624 def read(self):
1590 1625 # ignore HG_PENDING because identity is used only for writing
1591 1626 self.identity = util.filestat.frompath(
1592 1627 self._opener.join(self._filename)
1593 1628 )
1594 1629
1595 1630 try:
1596 1631 fp = self._opendirstatefile()
1597 1632 try:
1598 1633 st = fp.read()
1599 1634 finally:
1600 1635 fp.close()
1601 1636 except IOError as err:
1602 1637 if err.errno != errno.ENOENT:
1603 1638 raise
1604 1639 return
1605 1640 if not st:
1606 1641 return
1607 1642
1608 1643 if util.safehasattr(parsers, b'dict_new_presized'):
1609 1644 # Make an estimate of the number of files in the dirstate based on
1610 1645 # its size. From a linear regression on a set of real-world repos,
1611 1646 # all over 10,000 files, the size of a dirstate entry is 85
1612 1647 # bytes. The cost of resizing is significantly higher than the cost
1613 1648 # of filling in a larger presized dict, so subtract 20% from the
1614 1649 # size.
1615 1650 #
1616 1651 # This heuristic is imperfect in many ways, so in a future dirstate
1617 1652 # format update it makes sense to just record the number of entries
1618 1653 # on write.
1619 1654 self._map = parsers.dict_new_presized(len(st) // 71)
1620 1655
1621 1656 # Python's garbage collector triggers a GC each time a certain number
1622 1657 # of container objects (the number being defined by
1623 1658 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1624 1659 # for each file in the dirstate. The C version then immediately marks
1625 1660 # them as not to be tracked by the collector. However, this has no
1626 1661 # effect on when GCs are triggered, only on what objects the GC looks
1627 1662 # into. This means that O(number of files) GCs are unavoidable.
1628 1663 # Depending on when in the process's lifetime the dirstate is parsed,
1629 1664 # this can get very expensive. As a workaround, disable GC while
1630 1665 # parsing the dirstate.
1631 1666 #
1632 1667 # (we cannot decorate the function directly since it is in a C module)
1633 1668 parse_dirstate = util.nogc(parsers.parse_dirstate)
1634 1669 p = parse_dirstate(self._map, self.copymap, st)
1635 1670 if not self._dirtyparents:
1636 1671 self.setparents(*p)
1637 1672
1638 1673 # Avoid excess attribute lookups by fast pathing certain checks
1639 1674 self.__contains__ = self._map.__contains__
1640 1675 self.__getitem__ = self._map.__getitem__
1641 1676 self.get = self._map.get
1642 1677
1643 1678 def write(self, st, now):
1644 1679 st.write(
1645 1680 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1646 1681 )
1647 1682 st.close()
1648 1683 self._dirtyparents = False
1649 1684 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1650 1685
1651 1686 @propertycache
1652 1687 def nonnormalset(self):
1653 1688 nonnorm, otherparents = self.nonnormalentries()
1654 1689 self.otherparentset = otherparents
1655 1690 return nonnorm
1656 1691
1657 1692 @propertycache
1658 1693 def otherparentset(self):
1659 1694 nonnorm, otherparents = self.nonnormalentries()
1660 1695 self.nonnormalset = nonnorm
1661 1696 return otherparents
1662 1697
1663 1698 @propertycache
1664 1699 def identity(self):
1665 1700 self._map
1666 1701 return self.identity
1667 1702
1668 1703 @propertycache
1669 1704 def dirfoldmap(self):
1670 1705 f = {}
1671 1706 normcase = util.normcase
1672 1707 for name in self._dirs:
1673 1708 f[normcase(name)] = name
1674 1709 return f
1675 1710
1676 1711
1677 1712 if rustmod is not None:
1678 1713
1679 1714 class dirstatemap(object):
1680 1715 def __init__(self, ui, opener, root):
1681 1716 self._ui = ui
1682 1717 self._opener = opener
1683 1718 self._root = root
1684 1719 self._filename = b'dirstate'
1685 1720 self._parents = None
1686 1721 self._dirtyparents = False
1687 1722
1688 1723 # for consistent view between _pl() and _read() invocations
1689 1724 self._pendingmode = None
1690 1725
1691 1726 def addfile(self, *args, **kwargs):
1692 1727 return self._rustmap.addfile(*args, **kwargs)
1693 1728
1694 1729 def removefile(self, *args, **kwargs):
1695 1730 return self._rustmap.removefile(*args, **kwargs)
1696 1731
1697 1732 def dropfile(self, *args, **kwargs):
1698 1733 return self._rustmap.dropfile(*args, **kwargs)
1699 1734
1700 1735 def clearambiguoustimes(self, *args, **kwargs):
1701 1736 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1702 1737
1703 1738 def nonnormalentries(self):
1704 1739 return self._rustmap.nonnormalentries()
1705 1740
1706 1741 def get(self, *args, **kwargs):
1707 1742 return self._rustmap.get(*args, **kwargs)
1708 1743
1709 1744 @propertycache
1710 1745 def _rustmap(self):
1711 1746 self._rustmap = rustmod.DirstateMap(self._root)
1712 1747 self.read()
1713 1748 return self._rustmap
1714 1749
1715 1750 @property
1716 1751 def copymap(self):
1717 1752 return self._rustmap.copymap()
1718 1753
1719 1754 def preload(self):
1720 1755 self._rustmap
1721 1756
1722 1757 def clear(self):
1723 1758 self._rustmap.clear()
1724 1759 self.setparents(nullid, nullid)
1725 1760 util.clearcachedproperty(self, b"_dirs")
1726 1761 util.clearcachedproperty(self, b"_alldirs")
1727 1762 util.clearcachedproperty(self, b"dirfoldmap")
1728 1763
1729 1764 def items(self):
1730 1765 return self._rustmap.items()
1731 1766
1732 1767 def keys(self):
1733 1768 return iter(self._rustmap)
1734 1769
1735 1770 def __contains__(self, key):
1736 1771 return key in self._rustmap
1737 1772
1738 1773 def __getitem__(self, item):
1739 1774 return self._rustmap[item]
1740 1775
1741 1776 def __len__(self):
1742 1777 return len(self._rustmap)
1743 1778
1744 1779 def __iter__(self):
1745 1780 return iter(self._rustmap)
1746 1781
1747 1782 # forward for python2,3 compat
1748 1783 iteritems = items
1749 1784
1750 1785 def _opendirstatefile(self):
1751 1786 fp, mode = txnutil.trypending(
1752 1787 self._root, self._opener, self._filename
1753 1788 )
1754 1789 if self._pendingmode is not None and self._pendingmode != mode:
1755 1790 fp.close()
1756 1791 raise error.Abort(
1757 1792 _(b'working directory state may be changed parallelly')
1758 1793 )
1759 1794 self._pendingmode = mode
1760 1795 return fp
1761 1796
1762 1797 def setparents(self, p1, p2):
1763 1798 self._rustmap.setparents(p1, p2)
1764 1799 self._parents = (p1, p2)
1765 1800 self._dirtyparents = True
1766 1801
1767 1802 def parents(self):
1768 1803 if not self._parents:
1769 1804 try:
1770 1805 fp = self._opendirstatefile()
1771 1806 st = fp.read(40)
1772 1807 fp.close()
1773 1808 except IOError as err:
1774 1809 if err.errno != errno.ENOENT:
1775 1810 raise
1776 1811 # File doesn't exist, so the current state is empty
1777 1812 st = b''
1778 1813
1779 1814 try:
1780 1815 self._parents = self._rustmap.parents(st)
1781 1816 except ValueError:
1782 1817 raise error.Abort(
1783 1818 _(b'working directory state appears damaged!')
1784 1819 )
1785 1820
1786 1821 return self._parents
1787 1822
1788 1823 def read(self):
1789 1824 # ignore HG_PENDING because identity is used only for writing
1790 1825 self.identity = util.filestat.frompath(
1791 1826 self._opener.join(self._filename)
1792 1827 )
1793 1828
1794 1829 try:
1795 1830 fp = self._opendirstatefile()
1796 1831 try:
1797 1832 st = fp.read()
1798 1833 finally:
1799 1834 fp.close()
1800 1835 except IOError as err:
1801 1836 if err.errno != errno.ENOENT:
1802 1837 raise
1803 1838 return
1804 1839 if not st:
1805 1840 return
1806 1841
1807 1842 parse_dirstate = util.nogc(self._rustmap.read)
1808 1843 parents = parse_dirstate(st)
1809 1844 if parents and not self._dirtyparents:
1810 1845 self.setparents(*parents)
1811 1846
1812 1847 self.__contains__ = self._rustmap.__contains__
1813 1848 self.__getitem__ = self._rustmap.__getitem__
1814 1849 self.get = self._rustmap.get
1815 1850
1816 1851 def write(self, st, now):
1817 1852 parents = self.parents()
1818 1853 st.write(self._rustmap.write(parents[0], parents[1], now))
1819 1854 st.close()
1820 1855 self._dirtyparents = False
1821 1856
1822 1857 @propertycache
1823 1858 def filefoldmap(self):
1824 1859 """Returns a dictionary mapping normalized case paths to their
1825 1860 non-normalized versions.
1826 1861 """
1827 1862 return self._rustmap.filefoldmapasdict()
1828 1863
1829 1864 def hastrackeddir(self, d):
1830 1865 self._dirs # Trigger Python's propertycache
1831 1866 return self._rustmap.hastrackeddir(d)
1832 1867
1833 1868 def hasdir(self, d):
1834 1869 self._dirs # Trigger Python's propertycache
1835 1870 return self._rustmap.hasdir(d)
1836 1871
1837 1872 @propertycache
1838 1873 def _dirs(self):
1839 1874 return self._rustmap.getdirs()
1840 1875
1841 1876 @propertycache
1842 1877 def _alldirs(self):
1843 1878 return self._rustmap.getalldirs()
1844 1879
1845 1880 @propertycache
1846 1881 def identity(self):
1847 1882 self._rustmap
1848 1883 return self.identity
1849 1884
1850 1885 @property
1851 1886 def nonnormalset(self):
1852 1887 nonnorm = self._rustmap.non_normal_entries()
1853 1888 return nonnorm
1854 1889
1855 1890 @propertycache
1856 1891 def otherparentset(self):
1857 1892 otherparents = self._rustmap.other_parent_entries()
1858 1893 return otherparents
1859 1894
1860 1895 @propertycache
1861 1896 def dirfoldmap(self):
1862 1897 f = {}
1863 1898 normcase = util.normcase
1864 1899 for name in self._dirs:
1865 1900 f[normcase(name)] = name
1866 1901 return f
@@ -1,1601 +1,1604 b''
1 1 # match.py - filename matching
2 2 #
3 3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
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 from __future__ import absolute_import, print_function
9 9
10 10 import copy
11 11 import itertools
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .pycompat import open
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 pathutil,
21 21 policy,
22 22 pycompat,
23 23 util,
24 24 )
25 25 from .utils import stringutil
26 26
27 27 rustmod = policy.importrust('dirstate')
28 28
29 29 allpatternkinds = (
30 30 b're',
31 31 b'glob',
32 32 b'path',
33 33 b'relglob',
34 34 b'relpath',
35 35 b'relre',
36 36 b'rootglob',
37 37 b'listfile',
38 38 b'listfile0',
39 39 b'set',
40 40 b'include',
41 41 b'subinclude',
42 42 b'rootfilesin',
43 43 )
44 44 cwdrelativepatternkinds = (b'relpath', b'glob')
45 45
46 46 propertycache = util.propertycache
47 47
48 48
49 49 def _rematcher(regex):
50 50 '''compile the regexp with the best available regexp engine and return a
51 51 matcher function'''
52 52 m = util.re.compile(regex)
53 53 try:
54 54 # slightly faster, provided by facebook's re2 bindings
55 55 return m.test_match
56 56 except AttributeError:
57 57 return m.match
58 58
59 59
60 60 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
61 61 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
62 62 matchers = []
63 63 other = []
64 64
65 65 for kind, pat, source in kindpats:
66 66 if kind == b'set':
67 67 if ctx is None:
68 68 raise error.ProgrammingError(
69 69 b"fileset expression with no context"
70 70 )
71 71 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
72 72
73 73 if listsubrepos:
74 74 for subpath in ctx.substate:
75 75 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
76 76 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
77 77 matchers.append(pm)
78 78
79 79 continue
80 80 other.append((kind, pat, source))
81 81 return matchers, other
82 82
83 83
84 84 def _expandsubinclude(kindpats, root):
85 85 '''Returns the list of subinclude matcher args and the kindpats without the
86 86 subincludes in it.'''
87 87 relmatchers = []
88 88 other = []
89 89
90 90 for kind, pat, source in kindpats:
91 91 if kind == b'subinclude':
92 92 sourceroot = pathutil.dirname(util.normpath(source))
93 93 pat = util.pconvert(pat)
94 94 path = pathutil.join(sourceroot, pat)
95 95
96 96 newroot = pathutil.dirname(path)
97 97 matcherargs = (newroot, b'', [], [b'include:%s' % path])
98 98
99 99 prefix = pathutil.canonpath(root, root, newroot)
100 100 if prefix:
101 101 prefix += b'/'
102 102 relmatchers.append((prefix, matcherargs))
103 103 else:
104 104 other.append((kind, pat, source))
105 105
106 106 return relmatchers, other
107 107
108 108
109 109 def _kindpatsalwaysmatch(kindpats):
110 110 """"Checks whether the kindspats match everything, as e.g.
111 111 'relpath:.' does.
112 112 """
113 113 for kind, pat, source in kindpats:
114 114 if pat != b'' or kind not in [b'relpath', b'glob']:
115 115 return False
116 116 return True
117 117
118 118
119 119 def _buildkindpatsmatcher(
120 120 matchercls, root, cwd, kindpats, ctx=None, listsubrepos=False, badfn=None,
121 121 ):
122 122 matchers = []
123 123 fms, kindpats = _expandsets(
124 124 cwd, kindpats, ctx=ctx, listsubrepos=listsubrepos, badfn=badfn,
125 125 )
126 126 if kindpats:
127 127 m = matchercls(root, kindpats, badfn=badfn)
128 128 matchers.append(m)
129 129 if fms:
130 130 matchers.extend(fms)
131 131 if not matchers:
132 132 return nevermatcher(badfn=badfn)
133 133 if len(matchers) == 1:
134 134 return matchers[0]
135 135 return unionmatcher(matchers)
136 136
137 137
138 138 def match(
139 139 root,
140 140 cwd,
141 141 patterns=None,
142 142 include=None,
143 143 exclude=None,
144 144 default=b'glob',
145 145 auditor=None,
146 146 ctx=None,
147 147 listsubrepos=False,
148 148 warn=None,
149 149 badfn=None,
150 150 icasefs=False,
151 151 ):
152 152 r"""build an object to match a set of file patterns
153 153
154 154 arguments:
155 155 root - the canonical root of the tree you're matching against
156 156 cwd - the current working directory, if relevant
157 157 patterns - patterns to find
158 158 include - patterns to include (unless they are excluded)
159 159 exclude - patterns to exclude (even if they are included)
160 160 default - if a pattern in patterns has no explicit type, assume this one
161 161 auditor - optional path auditor
162 162 ctx - optional changecontext
163 163 listsubrepos - if True, recurse into subrepositories
164 164 warn - optional function used for printing warnings
165 165 badfn - optional bad() callback for this matcher instead of the default
166 166 icasefs - make a matcher for wdir on case insensitive filesystems, which
167 167 normalizes the given patterns to the case in the filesystem
168 168
169 169 a pattern is one of:
170 170 'glob:<glob>' - a glob relative to cwd
171 171 're:<regexp>' - a regular expression
172 172 'path:<path>' - a path relative to repository root, which is matched
173 173 recursively
174 174 'rootfilesin:<path>' - a path relative to repository root, which is
175 175 matched non-recursively (will not match subdirectories)
176 176 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
177 177 'relpath:<path>' - a path relative to cwd
178 178 'relre:<regexp>' - a regexp that needn't match the start of a name
179 179 'set:<fileset>' - a fileset expression
180 180 'include:<path>' - a file of patterns to read and include
181 181 'subinclude:<path>' - a file of patterns to match against files under
182 182 the same directory
183 183 '<something>' - a pattern of the specified default type
184 184
185 185 >>> def _match(root, *args, **kwargs):
186 186 ... return match(util.localpath(root), *args, **kwargs)
187 187
188 188 Usually a patternmatcher is returned:
189 189 >>> _match(b'/foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
190 190 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
191 191
192 192 Combining 'patterns' with 'include' (resp. 'exclude') gives an
193 193 intersectionmatcher (resp. a differencematcher):
194 194 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
195 195 <class 'mercurial.match.intersectionmatcher'>
196 196 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
197 197 <class 'mercurial.match.differencematcher'>
198 198
199 199 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
200 200 >>> _match(b'/foo', b'.', [])
201 201 <alwaysmatcher>
202 202
203 203 The 'default' argument determines which kind of pattern is assumed if a
204 204 pattern has no prefix:
205 205 >>> _match(b'/foo', b'.', [b'.*\.c$'], default=b're')
206 206 <patternmatcher patterns='.*\\.c$'>
207 207 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
208 208 <patternmatcher patterns='main\\.py(?:/|$)'>
209 209 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
210 210 <patternmatcher patterns='main.py'>
211 211
212 212 The primary use of matchers is to check whether a value (usually a file
213 213 name) matches againset one of the patterns given at initialization. There
214 214 are two ways of doing this check.
215 215
216 216 >>> m = _match(b'/foo', b'', [b're:.*\.c$', b'relpath:a'])
217 217
218 218 1. Calling the matcher with a file name returns True if any pattern
219 219 matches that file name:
220 220 >>> m(b'a')
221 221 True
222 222 >>> m(b'main.c')
223 223 True
224 224 >>> m(b'test.py')
225 225 False
226 226
227 227 2. Using the exact() method only returns True if the file name matches one
228 228 of the exact patterns (i.e. not re: or glob: patterns):
229 229 >>> m.exact(b'a')
230 230 True
231 231 >>> m.exact(b'main.c')
232 232 False
233 233 """
234 234 assert os.path.isabs(root)
235 235 cwd = os.path.join(root, util.localpath(cwd))
236 236 normalize = _donormalize
237 237 if icasefs:
238 238 dirstate = ctx.repo().dirstate
239 239 dsnormalize = dirstate.normalize
240 240
241 241 def normalize(patterns, default, root, cwd, auditor, warn):
242 242 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
243 243 kindpats = []
244 244 for kind, pats, source in kp:
245 245 if kind not in (b're', b'relre'): # regex can't be normalized
246 246 p = pats
247 247 pats = dsnormalize(pats)
248 248
249 249 # Preserve the original to handle a case only rename.
250 250 if p != pats and p in dirstate:
251 251 kindpats.append((kind, p, source))
252 252
253 253 kindpats.append((kind, pats, source))
254 254 return kindpats
255 255
256 256 if patterns:
257 257 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
258 258 if _kindpatsalwaysmatch(kindpats):
259 259 m = alwaysmatcher(badfn)
260 260 else:
261 261 m = _buildkindpatsmatcher(
262 262 patternmatcher,
263 263 root,
264 264 cwd,
265 265 kindpats,
266 266 ctx=ctx,
267 267 listsubrepos=listsubrepos,
268 268 badfn=badfn,
269 269 )
270 270 else:
271 271 # It's a little strange that no patterns means to match everything.
272 272 # Consider changing this to match nothing (probably using nevermatcher).
273 273 m = alwaysmatcher(badfn)
274 274
275 275 if include:
276 276 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
277 277 im = _buildkindpatsmatcher(
278 278 includematcher,
279 279 root,
280 280 cwd,
281 281 kindpats,
282 282 ctx=ctx,
283 283 listsubrepos=listsubrepos,
284 284 badfn=None,
285 285 )
286 286 m = intersectmatchers(m, im)
287 287 if exclude:
288 288 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
289 289 em = _buildkindpatsmatcher(
290 290 includematcher,
291 291 root,
292 292 cwd,
293 293 kindpats,
294 294 ctx=ctx,
295 295 listsubrepos=listsubrepos,
296 296 badfn=None,
297 297 )
298 298 m = differencematcher(m, em)
299 299 return m
300 300
301 301
302 302 def exact(files, badfn=None):
303 303 return exactmatcher(files, badfn=badfn)
304 304
305 305
306 306 def always(badfn=None):
307 307 return alwaysmatcher(badfn)
308 308
309 309
310 310 def never(badfn=None):
311 311 return nevermatcher(badfn)
312 312
313 313
314 314 def badmatch(match, badfn):
315 315 """Make a copy of the given matcher, replacing its bad method with the given
316 316 one.
317 317 """
318 318 m = copy.copy(match)
319 319 m.bad = badfn
320 320 return m
321 321
322 322
323 323 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
324 324 '''Convert 'kind:pat' from the patterns list to tuples with kind and
325 325 normalized and rooted patterns and with listfiles expanded.'''
326 326 kindpats = []
327 327 for kind, pat in [_patsplit(p, default) for p in patterns]:
328 328 if kind in cwdrelativepatternkinds:
329 329 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
330 330 elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
331 331 pat = util.normpath(pat)
332 332 elif kind in (b'listfile', b'listfile0'):
333 333 try:
334 334 files = util.readfile(pat)
335 335 if kind == b'listfile0':
336 336 files = files.split(b'\0')
337 337 else:
338 338 files = files.splitlines()
339 339 files = [f for f in files if f]
340 340 except EnvironmentError:
341 341 raise error.Abort(_(b"unable to read file list (%s)") % pat)
342 342 for k, p, source in _donormalize(
343 343 files, default, root, cwd, auditor, warn
344 344 ):
345 345 kindpats.append((k, p, pat))
346 346 continue
347 347 elif kind == b'include':
348 348 try:
349 349 fullpath = os.path.join(root, util.localpath(pat))
350 350 includepats = readpatternfile(fullpath, warn)
351 351 for k, p, source in _donormalize(
352 352 includepats, default, root, cwd, auditor, warn
353 353 ):
354 354 kindpats.append((k, p, source or pat))
355 355 except error.Abort as inst:
356 356 raise error.Abort(
357 357 b'%s: %s'
358 358 % (pat, inst[0]) # pytype: disable=unsupported-operands
359 359 )
360 360 except IOError as inst:
361 361 if warn:
362 362 warn(
363 363 _(b"skipping unreadable pattern file '%s': %s\n")
364 364 % (pat, stringutil.forcebytestr(inst.strerror))
365 365 )
366 366 continue
367 367 # else: re or relre - which cannot be normalized
368 368 kindpats.append((kind, pat, b''))
369 369 return kindpats
370 370
371 371
372 372 class basematcher(object):
373 373 def __init__(self, badfn=None):
374 374 if badfn is not None:
375 375 self.bad = badfn
376 376
377 377 def __call__(self, fn):
378 378 return self.matchfn(fn)
379 379
380 380 # Callbacks related to how the matcher is used by dirstate.walk.
381 381 # Subscribers to these events must monkeypatch the matcher object.
382 382 def bad(self, f, msg):
383 383 '''Callback from dirstate.walk for each explicit file that can't be
384 384 found/accessed, with an error message.'''
385 385
386 386 # If an traversedir is set, it will be called when a directory discovered
387 387 # by recursive traversal is visited.
388 388 traversedir = None
389 389
390 390 @propertycache
391 391 def _files(self):
392 392 return []
393 393
394 394 def files(self):
395 395 '''Explicitly listed files or patterns or roots:
396 396 if no patterns or .always(): empty list,
397 397 if exact: list exact files,
398 398 if not .anypats(): list all files and dirs,
399 399 else: optimal roots'''
400 400 return self._files
401 401
402 402 @propertycache
403 403 def _fileset(self):
404 404 return set(self._files)
405 405
406 406 def exact(self, f):
407 407 '''Returns True if f is in .files().'''
408 408 return f in self._fileset
409 409
410 410 def matchfn(self, f):
411 411 return False
412 412
413 413 def visitdir(self, dir):
414 414 '''Decides whether a directory should be visited based on whether it
415 415 has potential matches in it or one of its subdirectories. This is
416 416 based on the match's primary, included, and excluded patterns.
417 417
418 418 Returns the string 'all' if the given directory and all subdirectories
419 419 should be visited. Otherwise returns True or False indicating whether
420 420 the given directory should be visited.
421 421 '''
422 422 return True
423 423
424 424 def visitchildrenset(self, dir):
425 425 '''Decides whether a directory should be visited based on whether it
426 426 has potential matches in it or one of its subdirectories, and
427 427 potentially lists which subdirectories of that directory should be
428 428 visited. This is based on the match's primary, included, and excluded
429 429 patterns.
430 430
431 431 This function is very similar to 'visitdir', and the following mapping
432 432 can be applied:
433 433
434 434 visitdir | visitchildrenlist
435 435 ----------+-------------------
436 436 False | set()
437 437 'all' | 'all'
438 438 True | 'this' OR non-empty set of subdirs -or files- to visit
439 439
440 440 Example:
441 441 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
442 442 the following values (assuming the implementation of visitchildrenset
443 443 is capable of recognizing this; some implementations are not).
444 444
445 445 '' -> {'foo', 'qux'}
446 446 'baz' -> set()
447 447 'foo' -> {'bar'}
448 448 # Ideally this would be 'all', but since the prefix nature of matchers
449 449 # is applied to the entire matcher, we have to downgrade this to
450 450 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
451 451 # in.
452 452 'foo/bar' -> 'this'
453 453 'qux' -> 'this'
454 454
455 455 Important:
456 456 Most matchers do not know if they're representing files or
457 457 directories. They see ['path:dir/f'] and don't know whether 'f' is a
458 458 file or a directory, so visitchildrenset('dir') for most matchers will
459 459 return {'f'}, but if the matcher knows it's a file (like exactmatcher
460 460 does), it may return 'this'. Do not rely on the return being a set
461 461 indicating that there are no files in this dir to investigate (or
462 462 equivalently that if there are files to investigate in 'dir' that it
463 463 will always return 'this').
464 464 '''
465 465 return b'this'
466 466
467 467 def always(self):
468 468 '''Matcher will match everything and .files() will be empty --
469 469 optimization might be possible.'''
470 470 return False
471 471
472 472 def isexact(self):
473 473 '''Matcher will match exactly the list of files in .files() --
474 474 optimization might be possible.'''
475 475 return False
476 476
477 477 def prefix(self):
478 478 '''Matcher will match the paths in .files() recursively --
479 479 optimization might be possible.'''
480 480 return False
481 481
482 482 def anypats(self):
483 483 '''None of .always(), .isexact(), and .prefix() is true --
484 484 optimizations will be difficult.'''
485 485 return not self.always() and not self.isexact() and not self.prefix()
486 486
487 487
488 488 class alwaysmatcher(basematcher):
489 489 '''Matches everything.'''
490 490
491 491 def __init__(self, badfn=None):
492 492 super(alwaysmatcher, self).__init__(badfn)
493 493
494 494 def always(self):
495 495 return True
496 496
497 497 def matchfn(self, f):
498 498 return True
499 499
500 500 def visitdir(self, dir):
501 501 return b'all'
502 502
503 503 def visitchildrenset(self, dir):
504 504 return b'all'
505 505
506 506 def __repr__(self):
507 507 return r'<alwaysmatcher>'
508 508
509 509
510 510 class nevermatcher(basematcher):
511 511 '''Matches nothing.'''
512 512
513 513 def __init__(self, badfn=None):
514 514 super(nevermatcher, self).__init__(badfn)
515 515
516 516 # It's a little weird to say that the nevermatcher is an exact matcher
517 517 # or a prefix matcher, but it seems to make sense to let callers take
518 518 # fast paths based on either. There will be no exact matches, nor any
519 519 # prefixes (files() returns []), so fast paths iterating over them should
520 520 # be efficient (and correct).
521 521 def isexact(self):
522 522 return True
523 523
524 524 def prefix(self):
525 525 return True
526 526
527 527 def visitdir(self, dir):
528 528 return False
529 529
530 530 def visitchildrenset(self, dir):
531 531 return set()
532 532
533 533 def __repr__(self):
534 534 return r'<nevermatcher>'
535 535
536 536
537 537 class predicatematcher(basematcher):
538 538 """A matcher adapter for a simple boolean function"""
539 539
540 540 def __init__(self, predfn, predrepr=None, badfn=None):
541 541 super(predicatematcher, self).__init__(badfn)
542 542 self.matchfn = predfn
543 543 self._predrepr = predrepr
544 544
545 545 @encoding.strmethod
546 546 def __repr__(self):
547 547 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
548 548 self.matchfn
549 549 )
550 550 return b'<predicatenmatcher pred=%s>' % s
551 551
552 552
553 553 class patternmatcher(basematcher):
554 554 r"""Matches a set of (kind, pat, source) against a 'root' directory.
555 555
556 556 >>> kindpats = [
557 557 ... (b're', br'.*\.c$', b''),
558 558 ... (b'path', b'foo/a', b''),
559 559 ... (b'relpath', b'b', b''),
560 560 ... (b'glob', b'*.h', b''),
561 561 ... ]
562 562 >>> m = patternmatcher(b'foo', kindpats)
563 563 >>> m(b'main.c') # matches re:.*\.c$
564 564 True
565 565 >>> m(b'b.txt')
566 566 False
567 567 >>> m(b'foo/a') # matches path:foo/a
568 568 True
569 569 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
570 570 False
571 571 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
572 572 True
573 573 >>> m(b'lib.h') # matches glob:*.h
574 574 True
575 575
576 576 >>> m.files()
577 577 ['', 'foo/a', 'b', '']
578 578 >>> m.exact(b'foo/a')
579 579 True
580 580 >>> m.exact(b'b')
581 581 True
582 582 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
583 583 False
584 584 """
585 585
586 586 def __init__(self, root, kindpats, badfn=None):
587 587 super(patternmatcher, self).__init__(badfn)
588 588
589 589 self._files = _explicitfiles(kindpats)
590 590 self._prefix = _prefix(kindpats)
591 591 self._pats, self.matchfn = _buildmatch(kindpats, b'$', root)
592 592
593 593 @propertycache
594 594 def _dirs(self):
595 595 return set(pathutil.dirs(self._fileset))
596 596
597 597 def visitdir(self, dir):
598 598 if self._prefix and dir in self._fileset:
599 599 return b'all'
600 600 return (
601 601 dir in self._fileset
602 602 or dir in self._dirs
603 603 or any(
604 604 parentdir in self._fileset
605 605 for parentdir in pathutil.finddirs(dir)
606 606 )
607 607 )
608 608
609 609 def visitchildrenset(self, dir):
610 610 ret = self.visitdir(dir)
611 611 if ret is True:
612 612 return b'this'
613 613 elif not ret:
614 614 return set()
615 615 assert ret == b'all'
616 616 return b'all'
617 617
618 618 def prefix(self):
619 619 return self._prefix
620 620
621 621 @encoding.strmethod
622 622 def __repr__(self):
623 623 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
624 624
625 625
626 626 # This is basically a reimplementation of pathutil.dirs that stores the
627 627 # children instead of just a count of them, plus a small optional optimization
628 628 # to avoid some directories we don't need.
629 629 class _dirchildren(object):
630 630 def __init__(self, paths, onlyinclude=None):
631 631 self._dirs = {}
632 632 self._onlyinclude = onlyinclude or []
633 633 addpath = self.addpath
634 634 for f in paths:
635 635 addpath(f)
636 636
637 637 def addpath(self, path):
638 638 if path == b'':
639 639 return
640 640 dirs = self._dirs
641 641 findsplitdirs = _dirchildren._findsplitdirs
642 642 for d, b in findsplitdirs(path):
643 643 if d not in self._onlyinclude:
644 644 continue
645 645 dirs.setdefault(d, set()).add(b)
646 646
647 647 @staticmethod
648 648 def _findsplitdirs(path):
649 649 # yields (dirname, basename) tuples, walking back to the root. This is
650 650 # very similar to pathutil.finddirs, except:
651 651 # - produces a (dirname, basename) tuple, not just 'dirname'
652 652 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
653 653 # slash.
654 654 oldpos = len(path)
655 655 pos = path.rfind(b'/')
656 656 while pos != -1:
657 657 yield path[:pos], path[pos + 1 : oldpos]
658 658 oldpos = pos
659 659 pos = path.rfind(b'/', 0, pos)
660 660 yield b'', path[:oldpos]
661 661
662 662 def get(self, path):
663 663 return self._dirs.get(path, set())
664 664
665 665
666 666 class includematcher(basematcher):
667 667 def __init__(self, root, kindpats, badfn=None):
668 668 super(includematcher, self).__init__(badfn)
669
669 if rustmod is not None:
670 # We need to pass the patterns to Rust because they can contain
671 # patterns from the user interface
672 self._kindpats = kindpats
670 673 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
671 674 self._prefix = _prefix(kindpats)
672 675 roots, dirs, parents = _rootsdirsandparents(kindpats)
673 676 # roots are directories which are recursively included.
674 677 self._roots = set(roots)
675 678 # dirs are directories which are non-recursively included.
676 679 self._dirs = set(dirs)
677 680 # parents are directories which are non-recursively included because
678 681 # they are needed to get to items in _dirs or _roots.
679 682 self._parents = parents
680 683
681 684 def visitdir(self, dir):
682 685 if self._prefix and dir in self._roots:
683 686 return b'all'
684 687 return (
685 688 dir in self._roots
686 689 or dir in self._dirs
687 690 or dir in self._parents
688 691 or any(
689 692 parentdir in self._roots for parentdir in pathutil.finddirs(dir)
690 693 )
691 694 )
692 695
693 696 @propertycache
694 697 def _allparentschildren(self):
695 698 # It may seem odd that we add dirs, roots, and parents, and then
696 699 # restrict to only parents. This is to catch the case of:
697 700 # dirs = ['foo/bar']
698 701 # parents = ['foo']
699 702 # if we asked for the children of 'foo', but had only added
700 703 # self._parents, we wouldn't be able to respond ['bar'].
701 704 return _dirchildren(
702 705 itertools.chain(self._dirs, self._roots, self._parents),
703 706 onlyinclude=self._parents,
704 707 )
705 708
706 709 def visitchildrenset(self, dir):
707 710 if self._prefix and dir in self._roots:
708 711 return b'all'
709 712 # Note: this does *not* include the 'dir in self._parents' case from
710 713 # visitdir, that's handled below.
711 714 if (
712 715 b'' in self._roots
713 716 or dir in self._roots
714 717 or dir in self._dirs
715 718 or any(
716 719 parentdir in self._roots for parentdir in pathutil.finddirs(dir)
717 720 )
718 721 ):
719 722 return b'this'
720 723
721 724 if dir in self._parents:
722 725 return self._allparentschildren.get(dir) or set()
723 726 return set()
724 727
725 728 @encoding.strmethod
726 729 def __repr__(self):
727 730 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
728 731
729 732
730 733 class exactmatcher(basematcher):
731 734 r'''Matches the input files exactly. They are interpreted as paths, not
732 735 patterns (so no kind-prefixes).
733 736
734 737 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
735 738 >>> m(b'a.txt')
736 739 True
737 740 >>> m(b'b.txt')
738 741 False
739 742
740 743 Input files that would be matched are exactly those returned by .files()
741 744 >>> m.files()
742 745 ['a.txt', 're:.*\\.c$']
743 746
744 747 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
745 748 >>> m(b'main.c')
746 749 False
747 750 >>> m(br're:.*\.c$')
748 751 True
749 752 '''
750 753
751 754 def __init__(self, files, badfn=None):
752 755 super(exactmatcher, self).__init__(badfn)
753 756
754 757 if isinstance(files, list):
755 758 self._files = files
756 759 else:
757 760 self._files = list(files)
758 761
759 762 matchfn = basematcher.exact
760 763
761 764 @propertycache
762 765 def _dirs(self):
763 766 return set(pathutil.dirs(self._fileset))
764 767
765 768 def visitdir(self, dir):
766 769 return dir in self._dirs
767 770
768 771 def visitchildrenset(self, dir):
769 772 if not self._fileset or dir not in self._dirs:
770 773 return set()
771 774
772 775 candidates = self._fileset | self._dirs - {b''}
773 776 if dir != b'':
774 777 d = dir + b'/'
775 778 candidates = {c[len(d) :] for c in candidates if c.startswith(d)}
776 779 # self._dirs includes all of the directories, recursively, so if
777 780 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
778 781 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
779 782 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
780 783 # immediate subdir will be in there without a slash.
781 784 ret = {c for c in candidates if b'/' not in c}
782 785 # We really do not expect ret to be empty, since that would imply that
783 786 # there's something in _dirs that didn't have a file in _fileset.
784 787 assert ret
785 788 return ret
786 789
787 790 def isexact(self):
788 791 return True
789 792
790 793 @encoding.strmethod
791 794 def __repr__(self):
792 795 return b'<exactmatcher files=%r>' % self._files
793 796
794 797
795 798 class differencematcher(basematcher):
796 799 '''Composes two matchers by matching if the first matches and the second
797 800 does not.
798 801
799 802 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
800 803 '''
801 804
802 805 def __init__(self, m1, m2):
803 806 super(differencematcher, self).__init__()
804 807 self._m1 = m1
805 808 self._m2 = m2
806 809 self.bad = m1.bad
807 810 self.traversedir = m1.traversedir
808 811
809 812 def matchfn(self, f):
810 813 return self._m1(f) and not self._m2(f)
811 814
812 815 @propertycache
813 816 def _files(self):
814 817 if self.isexact():
815 818 return [f for f in self._m1.files() if self(f)]
816 819 # If m1 is not an exact matcher, we can't easily figure out the set of
817 820 # files, because its files() are not always files. For example, if
818 821 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
819 822 # want to remove "dir" from the set even though it would match m2,
820 823 # because the "dir" in m1 may not be a file.
821 824 return self._m1.files()
822 825
823 826 def visitdir(self, dir):
824 827 if self._m2.visitdir(dir) == b'all':
825 828 return False
826 829 elif not self._m2.visitdir(dir):
827 830 # m2 does not match dir, we can return 'all' here if possible
828 831 return self._m1.visitdir(dir)
829 832 return bool(self._m1.visitdir(dir))
830 833
831 834 def visitchildrenset(self, dir):
832 835 m2_set = self._m2.visitchildrenset(dir)
833 836 if m2_set == b'all':
834 837 return set()
835 838 m1_set = self._m1.visitchildrenset(dir)
836 839 # Possible values for m1: 'all', 'this', set(...), set()
837 840 # Possible values for m2: 'this', set(...), set()
838 841 # If m2 has nothing under here that we care about, return m1, even if
839 842 # it's 'all'. This is a change in behavior from visitdir, which would
840 843 # return True, not 'all', for some reason.
841 844 if not m2_set:
842 845 return m1_set
843 846 if m1_set in [b'all', b'this']:
844 847 # Never return 'all' here if m2_set is any kind of non-empty (either
845 848 # 'this' or set(foo)), since m2 might return set() for a
846 849 # subdirectory.
847 850 return b'this'
848 851 # Possible values for m1: set(...), set()
849 852 # Possible values for m2: 'this', set(...)
850 853 # We ignore m2's set results. They're possibly incorrect:
851 854 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
852 855 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
853 856 # return set(), which is *not* correct, we still need to visit 'dir'!
854 857 return m1_set
855 858
856 859 def isexact(self):
857 860 return self._m1.isexact()
858 861
859 862 @encoding.strmethod
860 863 def __repr__(self):
861 864 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
862 865
863 866
864 867 def intersectmatchers(m1, m2):
865 868 '''Composes two matchers by matching if both of them match.
866 869
867 870 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
868 871 '''
869 872 if m1 is None or m2 is None:
870 873 return m1 or m2
871 874 if m1.always():
872 875 m = copy.copy(m2)
873 876 # TODO: Consider encapsulating these things in a class so there's only
874 877 # one thing to copy from m1.
875 878 m.bad = m1.bad
876 879 m.traversedir = m1.traversedir
877 880 return m
878 881 if m2.always():
879 882 m = copy.copy(m1)
880 883 return m
881 884 return intersectionmatcher(m1, m2)
882 885
883 886
884 887 class intersectionmatcher(basematcher):
885 888 def __init__(self, m1, m2):
886 889 super(intersectionmatcher, self).__init__()
887 890 self._m1 = m1
888 891 self._m2 = m2
889 892 self.bad = m1.bad
890 893 self.traversedir = m1.traversedir
891 894
892 895 @propertycache
893 896 def _files(self):
894 897 if self.isexact():
895 898 m1, m2 = self._m1, self._m2
896 899 if not m1.isexact():
897 900 m1, m2 = m2, m1
898 901 return [f for f in m1.files() if m2(f)]
899 902 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
900 903 # the set of files, because their files() are not always files. For
901 904 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
902 905 # "path:dir2", we don't want to remove "dir2" from the set.
903 906 return self._m1.files() + self._m2.files()
904 907
905 908 def matchfn(self, f):
906 909 return self._m1(f) and self._m2(f)
907 910
908 911 def visitdir(self, dir):
909 912 visit1 = self._m1.visitdir(dir)
910 913 if visit1 == b'all':
911 914 return self._m2.visitdir(dir)
912 915 # bool() because visit1=True + visit2='all' should not be 'all'
913 916 return bool(visit1 and self._m2.visitdir(dir))
914 917
915 918 def visitchildrenset(self, dir):
916 919 m1_set = self._m1.visitchildrenset(dir)
917 920 if not m1_set:
918 921 return set()
919 922 m2_set = self._m2.visitchildrenset(dir)
920 923 if not m2_set:
921 924 return set()
922 925
923 926 if m1_set == b'all':
924 927 return m2_set
925 928 elif m2_set == b'all':
926 929 return m1_set
927 930
928 931 if m1_set == b'this' or m2_set == b'this':
929 932 return b'this'
930 933
931 934 assert isinstance(m1_set, set) and isinstance(m2_set, set)
932 935 return m1_set.intersection(m2_set)
933 936
934 937 def always(self):
935 938 return self._m1.always() and self._m2.always()
936 939
937 940 def isexact(self):
938 941 return self._m1.isexact() or self._m2.isexact()
939 942
940 943 @encoding.strmethod
941 944 def __repr__(self):
942 945 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
943 946
944 947
945 948 class subdirmatcher(basematcher):
946 949 """Adapt a matcher to work on a subdirectory only.
947 950
948 951 The paths are remapped to remove/insert the path as needed:
949 952
950 953 >>> from . import pycompat
951 954 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
952 955 >>> m2 = subdirmatcher(b'sub', m1)
953 956 >>> m2(b'a.txt')
954 957 False
955 958 >>> m2(b'b.txt')
956 959 True
957 960 >>> m2.matchfn(b'a.txt')
958 961 False
959 962 >>> m2.matchfn(b'b.txt')
960 963 True
961 964 >>> m2.files()
962 965 ['b.txt']
963 966 >>> m2.exact(b'b.txt')
964 967 True
965 968 >>> def bad(f, msg):
966 969 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
967 970 >>> m1.bad = bad
968 971 >>> m2.bad(b'x.txt', b'No such file')
969 972 sub/x.txt: No such file
970 973 """
971 974
972 975 def __init__(self, path, matcher):
973 976 super(subdirmatcher, self).__init__()
974 977 self._path = path
975 978 self._matcher = matcher
976 979 self._always = matcher.always()
977 980
978 981 self._files = [
979 982 f[len(path) + 1 :]
980 983 for f in matcher._files
981 984 if f.startswith(path + b"/")
982 985 ]
983 986
984 987 # If the parent repo had a path to this subrepo and the matcher is
985 988 # a prefix matcher, this submatcher always matches.
986 989 if matcher.prefix():
987 990 self._always = any(f == path for f in matcher._files)
988 991
989 992 def bad(self, f, msg):
990 993 self._matcher.bad(self._path + b"/" + f, msg)
991 994
992 995 def matchfn(self, f):
993 996 # Some information is lost in the superclass's constructor, so we
994 997 # can not accurately create the matching function for the subdirectory
995 998 # from the inputs. Instead, we override matchfn() and visitdir() to
996 999 # call the original matcher with the subdirectory path prepended.
997 1000 return self._matcher.matchfn(self._path + b"/" + f)
998 1001
999 1002 def visitdir(self, dir):
1000 1003 if dir == b'':
1001 1004 dir = self._path
1002 1005 else:
1003 1006 dir = self._path + b"/" + dir
1004 1007 return self._matcher.visitdir(dir)
1005 1008
1006 1009 def visitchildrenset(self, dir):
1007 1010 if dir == b'':
1008 1011 dir = self._path
1009 1012 else:
1010 1013 dir = self._path + b"/" + dir
1011 1014 return self._matcher.visitchildrenset(dir)
1012 1015
1013 1016 def always(self):
1014 1017 return self._always
1015 1018
1016 1019 def prefix(self):
1017 1020 return self._matcher.prefix() and not self._always
1018 1021
1019 1022 @encoding.strmethod
1020 1023 def __repr__(self):
1021 1024 return b'<subdirmatcher path=%r, matcher=%r>' % (
1022 1025 self._path,
1023 1026 self._matcher,
1024 1027 )
1025 1028
1026 1029
1027 1030 class prefixdirmatcher(basematcher):
1028 1031 """Adapt a matcher to work on a parent directory.
1029 1032
1030 1033 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1031 1034
1032 1035 The prefix path should usually be the relative path from the root of
1033 1036 this matcher to the root of the wrapped matcher.
1034 1037
1035 1038 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1036 1039 >>> m2 = prefixdirmatcher(b'd/e', m1)
1037 1040 >>> m2(b'a.txt')
1038 1041 False
1039 1042 >>> m2(b'd/e/a.txt')
1040 1043 True
1041 1044 >>> m2(b'd/e/b.txt')
1042 1045 False
1043 1046 >>> m2.files()
1044 1047 ['d/e/a.txt', 'd/e/f/b.txt']
1045 1048 >>> m2.exact(b'd/e/a.txt')
1046 1049 True
1047 1050 >>> m2.visitdir(b'd')
1048 1051 True
1049 1052 >>> m2.visitdir(b'd/e')
1050 1053 True
1051 1054 >>> m2.visitdir(b'd/e/f')
1052 1055 True
1053 1056 >>> m2.visitdir(b'd/e/g')
1054 1057 False
1055 1058 >>> m2.visitdir(b'd/ef')
1056 1059 False
1057 1060 """
1058 1061
1059 1062 def __init__(self, path, matcher, badfn=None):
1060 1063 super(prefixdirmatcher, self).__init__(badfn)
1061 1064 if not path:
1062 1065 raise error.ProgrammingError(b'prefix path must not be empty')
1063 1066 self._path = path
1064 1067 self._pathprefix = path + b'/'
1065 1068 self._matcher = matcher
1066 1069
1067 1070 @propertycache
1068 1071 def _files(self):
1069 1072 return [self._pathprefix + f for f in self._matcher._files]
1070 1073
1071 1074 def matchfn(self, f):
1072 1075 if not f.startswith(self._pathprefix):
1073 1076 return False
1074 1077 return self._matcher.matchfn(f[len(self._pathprefix) :])
1075 1078
1076 1079 @propertycache
1077 1080 def _pathdirs(self):
1078 1081 return set(pathutil.finddirs(self._path))
1079 1082
1080 1083 def visitdir(self, dir):
1081 1084 if dir == self._path:
1082 1085 return self._matcher.visitdir(b'')
1083 1086 if dir.startswith(self._pathprefix):
1084 1087 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1085 1088 return dir in self._pathdirs
1086 1089
1087 1090 def visitchildrenset(self, dir):
1088 1091 if dir == self._path:
1089 1092 return self._matcher.visitchildrenset(b'')
1090 1093 if dir.startswith(self._pathprefix):
1091 1094 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1092 1095 if dir in self._pathdirs:
1093 1096 return b'this'
1094 1097 return set()
1095 1098
1096 1099 def isexact(self):
1097 1100 return self._matcher.isexact()
1098 1101
1099 1102 def prefix(self):
1100 1103 return self._matcher.prefix()
1101 1104
1102 1105 @encoding.strmethod
1103 1106 def __repr__(self):
1104 1107 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1105 1108 pycompat.bytestr(self._path),
1106 1109 self._matcher,
1107 1110 )
1108 1111
1109 1112
1110 1113 class unionmatcher(basematcher):
1111 1114 """A matcher that is the union of several matchers.
1112 1115
1113 1116 The non-matching-attributes (bad, traversedir) are taken from the first
1114 1117 matcher.
1115 1118 """
1116 1119
1117 1120 def __init__(self, matchers):
1118 1121 m1 = matchers[0]
1119 1122 super(unionmatcher, self).__init__()
1120 1123 self.traversedir = m1.traversedir
1121 1124 self._matchers = matchers
1122 1125
1123 1126 def matchfn(self, f):
1124 1127 for match in self._matchers:
1125 1128 if match(f):
1126 1129 return True
1127 1130 return False
1128 1131
1129 1132 def visitdir(self, dir):
1130 1133 r = False
1131 1134 for m in self._matchers:
1132 1135 v = m.visitdir(dir)
1133 1136 if v == b'all':
1134 1137 return v
1135 1138 r |= v
1136 1139 return r
1137 1140
1138 1141 def visitchildrenset(self, dir):
1139 1142 r = set()
1140 1143 this = False
1141 1144 for m in self._matchers:
1142 1145 v = m.visitchildrenset(dir)
1143 1146 if not v:
1144 1147 continue
1145 1148 if v == b'all':
1146 1149 return v
1147 1150 if this or v == b'this':
1148 1151 this = True
1149 1152 # don't break, we might have an 'all' in here.
1150 1153 continue
1151 1154 assert isinstance(v, set)
1152 1155 r = r.union(v)
1153 1156 if this:
1154 1157 return b'this'
1155 1158 return r
1156 1159
1157 1160 @encoding.strmethod
1158 1161 def __repr__(self):
1159 1162 return b'<unionmatcher matchers=%r>' % self._matchers
1160 1163
1161 1164
1162 1165 def patkind(pattern, default=None):
1163 1166 r'''If pattern is 'kind:pat' with a known kind, return kind.
1164 1167
1165 1168 >>> patkind(br're:.*\.c$')
1166 1169 're'
1167 1170 >>> patkind(b'glob:*.c')
1168 1171 'glob'
1169 1172 >>> patkind(b'relpath:test.py')
1170 1173 'relpath'
1171 1174 >>> patkind(b'main.py')
1172 1175 >>> patkind(b'main.py', default=b're')
1173 1176 're'
1174 1177 '''
1175 1178 return _patsplit(pattern, default)[0]
1176 1179
1177 1180
1178 1181 def _patsplit(pattern, default):
1179 1182 """Split a string into the optional pattern kind prefix and the actual
1180 1183 pattern."""
1181 1184 if b':' in pattern:
1182 1185 kind, pat = pattern.split(b':', 1)
1183 1186 if kind in allpatternkinds:
1184 1187 return kind, pat
1185 1188 return default, pattern
1186 1189
1187 1190
1188 1191 def _globre(pat):
1189 1192 r'''Convert an extended glob string to a regexp string.
1190 1193
1191 1194 >>> from . import pycompat
1192 1195 >>> def bprint(s):
1193 1196 ... print(pycompat.sysstr(s))
1194 1197 >>> bprint(_globre(br'?'))
1195 1198 .
1196 1199 >>> bprint(_globre(br'*'))
1197 1200 [^/]*
1198 1201 >>> bprint(_globre(br'**'))
1199 1202 .*
1200 1203 >>> bprint(_globre(br'**/a'))
1201 1204 (?:.*/)?a
1202 1205 >>> bprint(_globre(br'a/**/b'))
1203 1206 a/(?:.*/)?b
1204 1207 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1205 1208 [a*?!^][\^b][^c]
1206 1209 >>> bprint(_globre(br'{a,b}'))
1207 1210 (?:a|b)
1208 1211 >>> bprint(_globre(br'.\*\?'))
1209 1212 \.\*\?
1210 1213 '''
1211 1214 i, n = 0, len(pat)
1212 1215 res = b''
1213 1216 group = 0
1214 1217 escape = util.stringutil.regexbytesescapemap.get
1215 1218
1216 1219 def peek():
1217 1220 return i < n and pat[i : i + 1]
1218 1221
1219 1222 while i < n:
1220 1223 c = pat[i : i + 1]
1221 1224 i += 1
1222 1225 if c not in b'*?[{},\\':
1223 1226 res += escape(c, c)
1224 1227 elif c == b'*':
1225 1228 if peek() == b'*':
1226 1229 i += 1
1227 1230 if peek() == b'/':
1228 1231 i += 1
1229 1232 res += b'(?:.*/)?'
1230 1233 else:
1231 1234 res += b'.*'
1232 1235 else:
1233 1236 res += b'[^/]*'
1234 1237 elif c == b'?':
1235 1238 res += b'.'
1236 1239 elif c == b'[':
1237 1240 j = i
1238 1241 if j < n and pat[j : j + 1] in b'!]':
1239 1242 j += 1
1240 1243 while j < n and pat[j : j + 1] != b']':
1241 1244 j += 1
1242 1245 if j >= n:
1243 1246 res += b'\\['
1244 1247 else:
1245 1248 stuff = pat[i:j].replace(b'\\', b'\\\\')
1246 1249 i = j + 1
1247 1250 if stuff[0:1] == b'!':
1248 1251 stuff = b'^' + stuff[1:]
1249 1252 elif stuff[0:1] == b'^':
1250 1253 stuff = b'\\' + stuff
1251 1254 res = b'%s[%s]' % (res, stuff)
1252 1255 elif c == b'{':
1253 1256 group += 1
1254 1257 res += b'(?:'
1255 1258 elif c == b'}' and group:
1256 1259 res += b')'
1257 1260 group -= 1
1258 1261 elif c == b',' and group:
1259 1262 res += b'|'
1260 1263 elif c == b'\\':
1261 1264 p = peek()
1262 1265 if p:
1263 1266 i += 1
1264 1267 res += escape(p, p)
1265 1268 else:
1266 1269 res += escape(c, c)
1267 1270 else:
1268 1271 res += escape(c, c)
1269 1272 return res
1270 1273
1271 1274
1272 1275 def _regex(kind, pat, globsuffix):
1273 1276 '''Convert a (normalized) pattern of any kind into a
1274 1277 regular expression.
1275 1278 globsuffix is appended to the regexp of globs.'''
1276 1279 if not pat and kind in (b'glob', b'relpath'):
1277 1280 return b''
1278 1281 if kind == b're':
1279 1282 return pat
1280 1283 if kind in (b'path', b'relpath'):
1281 1284 if pat == b'.':
1282 1285 return b''
1283 1286 return util.stringutil.reescape(pat) + b'(?:/|$)'
1284 1287 if kind == b'rootfilesin':
1285 1288 if pat == b'.':
1286 1289 escaped = b''
1287 1290 else:
1288 1291 # Pattern is a directory name.
1289 1292 escaped = util.stringutil.reescape(pat) + b'/'
1290 1293 # Anything after the pattern must be a non-directory.
1291 1294 return escaped + b'[^/]+$'
1292 1295 if kind == b'relglob':
1293 1296 globre = _globre(pat)
1294 1297 if globre.startswith(b'[^/]*'):
1295 1298 # When pat has the form *XYZ (common), make the returned regex more
1296 1299 # legible by returning the regex for **XYZ instead of **/*XYZ.
1297 1300 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1298 1301 return b'(?:|.*/)' + globre + globsuffix
1299 1302 if kind == b'relre':
1300 1303 if pat.startswith(b'^'):
1301 1304 return pat
1302 1305 return b'.*' + pat
1303 1306 if kind in (b'glob', b'rootglob'):
1304 1307 return _globre(pat) + globsuffix
1305 1308 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1306 1309
1307 1310
1308 1311 def _buildmatch(kindpats, globsuffix, root):
1309 1312 '''Return regexp string and a matcher function for kindpats.
1310 1313 globsuffix is appended to the regexp of globs.'''
1311 1314 matchfuncs = []
1312 1315
1313 1316 subincludes, kindpats = _expandsubinclude(kindpats, root)
1314 1317 if subincludes:
1315 1318 submatchers = {}
1316 1319
1317 1320 def matchsubinclude(f):
1318 1321 for prefix, matcherargs in subincludes:
1319 1322 if f.startswith(prefix):
1320 1323 mf = submatchers.get(prefix)
1321 1324 if mf is None:
1322 1325 mf = match(*matcherargs)
1323 1326 submatchers[prefix] = mf
1324 1327
1325 1328 if mf(f[len(prefix) :]):
1326 1329 return True
1327 1330 return False
1328 1331
1329 1332 matchfuncs.append(matchsubinclude)
1330 1333
1331 1334 regex = b''
1332 1335 if kindpats:
1333 1336 if all(k == b'rootfilesin' for k, p, s in kindpats):
1334 1337 dirs = {p for k, p, s in kindpats}
1335 1338
1336 1339 def mf(f):
1337 1340 i = f.rfind(b'/')
1338 1341 if i >= 0:
1339 1342 dir = f[:i]
1340 1343 else:
1341 1344 dir = b'.'
1342 1345 return dir in dirs
1343 1346
1344 1347 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1345 1348 matchfuncs.append(mf)
1346 1349 else:
1347 1350 regex, mf = _buildregexmatch(kindpats, globsuffix)
1348 1351 matchfuncs.append(mf)
1349 1352
1350 1353 if len(matchfuncs) == 1:
1351 1354 return regex, matchfuncs[0]
1352 1355 else:
1353 1356 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1354 1357
1355 1358
1356 1359 MAX_RE_SIZE = 20000
1357 1360
1358 1361
1359 1362 def _joinregexes(regexps):
1360 1363 """gather multiple regular expressions into a single one"""
1361 1364 return b'|'.join(regexps)
1362 1365
1363 1366
1364 1367 def _buildregexmatch(kindpats, globsuffix):
1365 1368 """Build a match function from a list of kinds and kindpats,
1366 1369 return regexp string and a matcher function.
1367 1370
1368 1371 Test too large input
1369 1372 >>> _buildregexmatch([
1370 1373 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1371 1374 ... ], b'$')
1372 1375 Traceback (most recent call last):
1373 1376 ...
1374 1377 Abort: matcher pattern is too long (20009 bytes)
1375 1378 """
1376 1379 try:
1377 1380 allgroups = []
1378 1381 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1379 1382 fullregexp = _joinregexes(regexps)
1380 1383
1381 1384 startidx = 0
1382 1385 groupsize = 0
1383 1386 for idx, r in enumerate(regexps):
1384 1387 piecesize = len(r)
1385 1388 if piecesize > MAX_RE_SIZE:
1386 1389 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1387 1390 raise error.Abort(msg)
1388 1391 elif (groupsize + piecesize) > MAX_RE_SIZE:
1389 1392 group = regexps[startidx:idx]
1390 1393 allgroups.append(_joinregexes(group))
1391 1394 startidx = idx
1392 1395 groupsize = 0
1393 1396 groupsize += piecesize + 1
1394 1397
1395 1398 if startidx == 0:
1396 1399 matcher = _rematcher(fullregexp)
1397 1400 func = lambda s: bool(matcher(s))
1398 1401 else:
1399 1402 group = regexps[startidx:]
1400 1403 allgroups.append(_joinregexes(group))
1401 1404 allmatchers = [_rematcher(g) for g in allgroups]
1402 1405 func = lambda s: any(m(s) for m in allmatchers)
1403 1406 return fullregexp, func
1404 1407 except re.error:
1405 1408 for k, p, s in kindpats:
1406 1409 try:
1407 1410 _rematcher(_regex(k, p, globsuffix))
1408 1411 except re.error:
1409 1412 if s:
1410 1413 raise error.Abort(
1411 1414 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1412 1415 )
1413 1416 else:
1414 1417 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1415 1418 raise error.Abort(_(b"invalid pattern"))
1416 1419
1417 1420
1418 1421 def _patternrootsanddirs(kindpats):
1419 1422 '''Returns roots and directories corresponding to each pattern.
1420 1423
1421 1424 This calculates the roots and directories exactly matching the patterns and
1422 1425 returns a tuple of (roots, dirs) for each. It does not return other
1423 1426 directories which may also need to be considered, like the parent
1424 1427 directories.
1425 1428 '''
1426 1429 r = []
1427 1430 d = []
1428 1431 for kind, pat, source in kindpats:
1429 1432 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1430 1433 root = []
1431 1434 for p in pat.split(b'/'):
1432 1435 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1433 1436 break
1434 1437 root.append(p)
1435 1438 r.append(b'/'.join(root))
1436 1439 elif kind in (b'relpath', b'path'):
1437 1440 if pat == b'.':
1438 1441 pat = b''
1439 1442 r.append(pat)
1440 1443 elif kind in (b'rootfilesin',):
1441 1444 if pat == b'.':
1442 1445 pat = b''
1443 1446 d.append(pat)
1444 1447 else: # relglob, re, relre
1445 1448 r.append(b'')
1446 1449 return r, d
1447 1450
1448 1451
1449 1452 def _roots(kindpats):
1450 1453 '''Returns root directories to match recursively from the given patterns.'''
1451 1454 roots, dirs = _patternrootsanddirs(kindpats)
1452 1455 return roots
1453 1456
1454 1457
1455 1458 def _rootsdirsandparents(kindpats):
1456 1459 '''Returns roots and exact directories from patterns.
1457 1460
1458 1461 `roots` are directories to match recursively, `dirs` should
1459 1462 be matched non-recursively, and `parents` are the implicitly required
1460 1463 directories to walk to items in either roots or dirs.
1461 1464
1462 1465 Returns a tuple of (roots, dirs, parents).
1463 1466
1464 1467 >>> r = _rootsdirsandparents(
1465 1468 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1466 1469 ... (b'glob', b'g*', b'')])
1467 1470 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1468 1471 (['g/h', 'g/h', ''], []) ['', 'g']
1469 1472 >>> r = _rootsdirsandparents(
1470 1473 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1471 1474 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1472 1475 ([], ['g/h', '']) ['', 'g']
1473 1476 >>> r = _rootsdirsandparents(
1474 1477 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1475 1478 ... (b'path', b'', b'')])
1476 1479 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1477 1480 (['r', 'p/p', ''], []) ['', 'p']
1478 1481 >>> r = _rootsdirsandparents(
1479 1482 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1480 1483 ... (b'relre', b'rr', b'')])
1481 1484 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1482 1485 (['', '', ''], []) ['']
1483 1486 '''
1484 1487 r, d = _patternrootsanddirs(kindpats)
1485 1488
1486 1489 p = set()
1487 1490 # Add the parents as non-recursive/exact directories, since they must be
1488 1491 # scanned to get to either the roots or the other exact directories.
1489 1492 p.update(pathutil.dirs(d))
1490 1493 p.update(pathutil.dirs(r))
1491 1494
1492 1495 # FIXME: all uses of this function convert these to sets, do so before
1493 1496 # returning.
1494 1497 # FIXME: all uses of this function do not need anything in 'roots' and
1495 1498 # 'dirs' to also be in 'parents', consider removing them before returning.
1496 1499 return r, d, p
1497 1500
1498 1501
1499 1502 def _explicitfiles(kindpats):
1500 1503 '''Returns the potential explicit filenames from the patterns.
1501 1504
1502 1505 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1503 1506 ['foo/bar']
1504 1507 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1505 1508 []
1506 1509 '''
1507 1510 # Keep only the pattern kinds where one can specify filenames (vs only
1508 1511 # directory names).
1509 1512 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1510 1513 return _roots(filable)
1511 1514
1512 1515
1513 1516 def _prefix(kindpats):
1514 1517 '''Whether all the patterns match a prefix (i.e. recursively)'''
1515 1518 for kind, pat, source in kindpats:
1516 1519 if kind not in (b'path', b'relpath'):
1517 1520 return False
1518 1521 return True
1519 1522
1520 1523
1521 1524 _commentre = None
1522 1525
1523 1526
1524 1527 def readpatternfile(filepath, warn, sourceinfo=False):
1525 1528 '''parse a pattern file, returning a list of
1526 1529 patterns. These patterns should be given to compile()
1527 1530 to be validated and converted into a match function.
1528 1531
1529 1532 trailing white space is dropped.
1530 1533 the escape character is backslash.
1531 1534 comments start with #.
1532 1535 empty lines are skipped.
1533 1536
1534 1537 lines can be of the following formats:
1535 1538
1536 1539 syntax: regexp # defaults following lines to non-rooted regexps
1537 1540 syntax: glob # defaults following lines to non-rooted globs
1538 1541 re:pattern # non-rooted regular expression
1539 1542 glob:pattern # non-rooted glob
1540 1543 rootglob:pat # rooted glob (same root as ^ in regexps)
1541 1544 pattern # pattern of the current default type
1542 1545
1543 1546 if sourceinfo is set, returns a list of tuples:
1544 1547 (pattern, lineno, originalline).
1545 1548 This is useful to debug ignore patterns.
1546 1549 '''
1547 1550
1548 1551 syntaxes = {
1549 1552 b're': b'relre:',
1550 1553 b'regexp': b'relre:',
1551 1554 b'glob': b'relglob:',
1552 1555 b'rootglob': b'rootglob:',
1553 1556 b'include': b'include',
1554 1557 b'subinclude': b'subinclude',
1555 1558 }
1556 1559 syntax = b'relre:'
1557 1560 patterns = []
1558 1561
1559 1562 fp = open(filepath, b'rb')
1560 1563 for lineno, line in enumerate(util.iterfile(fp), start=1):
1561 1564 if b"#" in line:
1562 1565 global _commentre
1563 1566 if not _commentre:
1564 1567 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1565 1568 # remove comments prefixed by an even number of escapes
1566 1569 m = _commentre.search(line)
1567 1570 if m:
1568 1571 line = line[: m.end(1)]
1569 1572 # fixup properly escaped comments that survived the above
1570 1573 line = line.replace(b"\\#", b"#")
1571 1574 line = line.rstrip()
1572 1575 if not line:
1573 1576 continue
1574 1577
1575 1578 if line.startswith(b'syntax:'):
1576 1579 s = line[7:].strip()
1577 1580 try:
1578 1581 syntax = syntaxes[s]
1579 1582 except KeyError:
1580 1583 if warn:
1581 1584 warn(
1582 1585 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1583 1586 )
1584 1587 continue
1585 1588
1586 1589 linesyntax = syntax
1587 1590 for s, rels in pycompat.iteritems(syntaxes):
1588 1591 if line.startswith(rels):
1589 1592 linesyntax = rels
1590 1593 line = line[len(rels) :]
1591 1594 break
1592 1595 elif line.startswith(s + b':'):
1593 1596 linesyntax = rels
1594 1597 line = line[len(s) + 1 :]
1595 1598 break
1596 1599 if sourceinfo:
1597 1600 patterns.append((linesyntax + line, lineno, line))
1598 1601 else:
1599 1602 patterns.append(linesyntax + line)
1600 1603 fp.close()
1601 1604 return patterns
@@ -1,1177 +1,1182 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extdiff]
3 3 > # for portability:
4 4 > pdiff = sh "$RUNTESTDIR/pdiff"
5 5 > [progress]
6 6 > disable=False
7 7 > assume-tty = 1
8 8 > delay = 0
9 9 > # set changedelay really large so we don't see nested topics
10 10 > changedelay = 30000
11 11 > format = topic bar number
12 12 > refresh = 0
13 13 > width = 60
14 14 > EOF
15 15
16 16 Preparing the subrepository 'sub2'
17 17
18 18 $ hg init sub2
19 19 $ echo sub2 > sub2/sub2
20 20 $ hg add -R sub2
21 21 adding sub2/sub2
22 22 $ hg commit -R sub2 -m "sub2 import"
23 23
24 24 Preparing the 'sub1' repo which depends on the subrepo 'sub2'
25 25
26 26 $ hg init sub1
27 27 $ echo sub1 > sub1/sub1
28 28 $ echo "sub2 = ../sub2" > sub1/.hgsub
29 29 $ hg clone sub2 sub1/sub2
30 30 \r (no-eol) (esc)
31 31 linking [ <=> ] 1\r (no-eol) (esc)
32 32 linking [ <=> ] 2\r (no-eol) (esc)
33 33 linking [ <=> ] 3\r (no-eol) (esc)
34 34 linking [ <=> ] 4\r (no-eol) (esc)
35 35 linking [ <=> ] 5\r (no-eol) (esc)
36 36 linking [ <=> ] 6\r (no-eol) (esc)
37 37 \r (no-eol) (esc)
38 38 \r (no-eol) (esc)
39 39 updating [===========================================>] 1/1\r (no-eol) (esc)
40 40 \r (no-eol) (esc)
41 41 updating to branch default
42 42 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
43 43 $ hg add -R sub1
44 44 adding sub1/.hgsub
45 45 adding sub1/sub1
46 46 $ hg commit -R sub1 -m "sub1 import"
47 47
48 48 Preparing the 'main' repo which depends on the subrepo 'sub1'
49 49
50 50 $ hg init main
51 51 $ echo main > main/main
52 52 $ echo "sub1 = ../sub1" > main/.hgsub
53 53 $ hg clone sub1 main/sub1
54 54 \r (no-eol) (esc)
55 55 linking [ <=> ] 1\r (no-eol) (esc)
56 56 linking [ <=> ] 2\r (no-eol) (esc)
57 57 linking [ <=> ] 3\r (no-eol) (esc)
58 58 linking [ <=> ] 4\r (no-eol) (esc)
59 59 linking [ <=> ] 5\r (no-eol) (esc)
60 60 linking [ <=> ] 6\r (no-eol) (esc)
61 61 linking [ <=> ] 7\r (no-eol) (esc)
62 62 linking [ <=> ] 8\r (no-eol) (esc)
63 63 linking [ <=> ] 9\r (no-eol) (esc) (reposimplestore !)
64 64 linking [ <=> ] 10\r (no-eol) (esc) (reposimplestore !)
65 65 \r (no-eol) (esc)
66 66 \r (no-eol) (esc)
67 67 updating [===========================================>] 3/3\r (no-eol) (esc)
68 68 \r (no-eol) (esc)
69 69 \r (no-eol) (esc)
70 70 linking [ <=> ] 1\r (no-eol) (esc)
71 71 linking [ <=> ] 2\r (no-eol) (esc)
72 72 linking [ <=> ] 3\r (no-eol) (esc)
73 73 linking [ <=> ] 4\r (no-eol) (esc)
74 74 linking [ <=> ] 5\r (no-eol) (esc)
75 75 linking [ <=> ] 6\r (no-eol) (esc)
76 76 updating [===========================================>] 1/1\r (no-eol) (esc)
77 77 \r (no-eol) (esc)
78 78 updating to branch default
79 79 cloning subrepo sub2 from $TESTTMP/sub2
80 80 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
81 81 $ hg add -R main
82 82 adding main/.hgsub
83 83 adding main/main
84 84 $ hg commit -R main -m "main import"
85 85
86 86 #if serve
87 87
88 88 Unfortunately, subrepos not at their nominal location cannot be cloned. But
89 89 they are still served from their location within the local repository. The only
90 90 reason why 'main' can be cloned via the filesystem is because 'sub1' and 'sub2'
91 91 are also available as siblings of 'main'.
92 92
93 93 $ hg serve -R main --debug -S -p $HGPORT -d --pid-file=hg1.pid -E error.log -A access.log
94 94 adding = $TESTTMP/main
95 95 adding sub1 = $TESTTMP/main/sub1
96 96 adding sub1/sub2 = $TESTTMP/main/sub1/sub2
97 97 listening at http://*:$HGPORT/ (bound to *:$HGPORT) (glob) (?)
98 98 adding = $TESTTMP/main (?)
99 99 adding sub1 = $TESTTMP/main/sub1 (?)
100 100 adding sub1/sub2 = $TESTTMP/main/sub1/sub2 (?)
101 101 $ cat hg1.pid >> $DAEMON_PIDS
102 102
103 103 $ hg clone http://localhost:$HGPORT httpclone --config progress.disable=True
104 104 requesting all changes
105 105 adding changesets
106 106 adding manifests
107 107 adding file changes
108 108 added 1 changesets with 3 changes to 3 files
109 109 new changesets 7f491f53a367
110 110 updating to branch default
111 111 cloning subrepo sub1 from http://localhost:$HGPORT/../sub1
112 112 abort: HTTP Error 404: Not Found
113 113 [255]
114 114
115 115 $ cat access.log
116 116 * "GET /?cmd=capabilities HTTP/1.1" 200 - (glob)
117 117 * "GET /?cmd=batch HTTP/1.1" 200 - * (glob)
118 118 * "GET /?cmd=getbundle HTTP/1.1" 200 - * (glob)
119 119 * "GET /../sub1?cmd=capabilities HTTP/1.1" 404 - (glob)
120 120 $ cat error.log
121 121
122 122 $ killdaemons.py
123 123 $ rm hg1.pid error.log access.log
124 124 #endif
125 125
126 126 Cleaning both repositories, just as a clone -U
127 127
128 128 $ hg up -C -R sub2 null
129 129 \r (no-eol) (esc)
130 130 updating [===========================================>] 1/1\r (no-eol) (esc)
131 131 \r (no-eol) (esc)
132 132 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
133 133 $ hg up -C -R sub1 null
134 134 \r (no-eol) (esc)
135 135 updating [===========================================>] 1/1\r (no-eol) (esc)
136 136 \r (no-eol) (esc)
137 137 \r (no-eol) (esc)
138 138 updating [===========================================>] 3/3\r (no-eol) (esc)
139 139 \r (no-eol) (esc)
140 140 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
141 141 $ hg up -C -R main null
142 142 \r (no-eol) (esc)
143 143 updating [===========================================>] 1/1\r (no-eol) (esc)
144 144 \r (no-eol) (esc)
145 145 \r (no-eol) (esc)
146 146 updating [===========================================>] 3/3\r (no-eol) (esc)
147 147 \r (no-eol) (esc)
148 148 \r (no-eol) (esc)
149 149 updating [===========================================>] 3/3\r (no-eol) (esc)
150 150 \r (no-eol) (esc)
151 151 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
152 152 $ rm -rf main/sub1
153 153 $ rm -rf sub1/sub2
154 154
155 155 Clone main
156 156
157 157 $ hg --config extensions.largefiles= clone main cloned
158 158 \r (no-eol) (esc)
159 159 linking [ <=> ] 1\r (no-eol) (esc)
160 160 linking [ <=> ] 2\r (no-eol) (esc)
161 161 linking [ <=> ] 3\r (no-eol) (esc)
162 162 linking [ <=> ] 4\r (no-eol) (esc)
163 163 linking [ <=> ] 5\r (no-eol) (esc)
164 164 linking [ <=> ] 6\r (no-eol) (esc)
165 165 linking [ <=> ] 7\r (no-eol) (esc)
166 166 linking [ <=> ] 8\r (no-eol) (esc)
167 167 linking [ <=> ] 9\r (no-eol) (esc) (reposimplestore !)
168 168 linking [ <=> ] 10\r (no-eol) (esc) (reposimplestore !)
169 169 \r (no-eol) (esc)
170 170 \r (no-eol) (esc)
171 171 updating [===========================================>] 3/3\r (no-eol) (esc)
172 172 \r (no-eol) (esc)
173 173 \r (no-eol) (esc)
174 174 linking [ <=> ] 1\r (no-eol) (esc)
175 175 linking [ <=> ] 2\r (no-eol) (esc)
176 176 linking [ <=> ] 3\r (no-eol) (esc)
177 177 linking [ <=> ] 4\r (no-eol) (esc)
178 178 linking [ <=> ] 5\r (no-eol) (esc)
179 179 linking [ <=> ] 6\r (no-eol) (esc)
180 180 linking [ <=> ] 7\r (no-eol) (esc)
181 181 linking [ <=> ] 8\r (no-eol) (esc)
182 182 linking [ <=> ] 9\r (no-eol) (esc) (reposimplestore !)
183 183 linking [ <=> ] 10\r (no-eol) (esc) (reposimplestore !)
184 184 updating [===========================================>] 3/3\r (no-eol) (esc)
185 185 \r (no-eol) (esc)
186 186 \r (no-eol) (esc)
187 187 linking [ <=> ] 1\r (no-eol) (esc) (reporevlogstore !)
188 188 linking [ <=> ] 2\r (no-eol) (esc) (reporevlogstore !)
189 189 linking [ <=> ] 3\r (no-eol) (esc) (reporevlogstore !)
190 190 linking [ <=> ] 4\r (no-eol) (esc) (reporevlogstore !)
191 191 linking [ <=> ] 5\r (no-eol) (esc) (reporevlogstore !)
192 192 linking [ <=> ] 6\r (no-eol) (esc) (reporevlogstore !)
193 193 linking [ <=> ] 1\r (no-eol) (esc) (reposimplestore !)
194 194 linking [ <=> ] 2\r (no-eol) (esc) (reposimplestore !)
195 195 linking [ <=> ] 3\r (no-eol) (esc) (reposimplestore !)
196 196 linking [ <=> ] 4\r (no-eol) (esc) (reposimplestore !)
197 197 linking [ <=> ] 5\r (no-eol) (esc) (reposimplestore !)
198 198 linking [ <=> ] 6\r (no-eol) (esc) (reposimplestore !)
199 199 updating [===========================================>] 1/1\r (no-eol) (esc)
200 200 \r (no-eol) (esc)
201 201 updating to branch default
202 202 cloning subrepo sub1 from $TESTTMP/sub1
203 203 cloning subrepo sub1/sub2 from $TESTTMP/sub2
204 204 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
205 205
206 206 Largefiles is NOT enabled in the clone if the source repo doesn't require it
207 207 $ grep largefiles cloned/.hg/hgrc
208 208 [1]
209 209
210 210 Checking cloned repo ids
211 211
212 212 $ printf "cloned " ; hg id -R cloned
213 213 cloned 7f491f53a367 tip
214 214 $ printf "cloned/sub1 " ; hg id -R cloned/sub1
215 215 cloned/sub1 fc3b4ce2696f tip
216 216 $ printf "cloned/sub1/sub2 " ; hg id -R cloned/sub1/sub2
217 217 cloned/sub1/sub2 c57a0840e3ba tip
218 218
219 219 debugsub output for main and sub1
220 220
221 221 $ hg debugsub -R cloned
222 222 path sub1
223 223 source ../sub1
224 224 revision fc3b4ce2696f7741438c79207583768f2ce6b0dd
225 225 $ hg debugsub -R cloned/sub1
226 226 path sub2
227 227 source ../sub2
228 228 revision c57a0840e3badd667ef3c3ef65471609acb2ba3c
229 229
230 230 Modifying deeply nested 'sub2'
231 231
232 232 $ echo modified > cloned/sub1/sub2/sub2
233 233 $ hg commit --subrepos -m "deep nested modif should trigger a commit" -R cloned
234 234 committing subrepository sub1
235 235 committing subrepository sub1/sub2
236 236
237 237 Checking modified node ids
238 238
239 239 $ printf "cloned " ; hg id -R cloned
240 240 cloned ffe6649062fe tip
241 241 $ printf "cloned/sub1 " ; hg id -R cloned/sub1
242 242 cloned/sub1 2ecb03bf44a9 tip
243 243 $ printf "cloned/sub1/sub2 " ; hg id -R cloned/sub1/sub2
244 244 cloned/sub1/sub2 53dd3430bcaf tip
245 245
246 246 debugsub output for main and sub1
247 247
248 248 $ hg debugsub -R cloned
249 249 path sub1
250 250 source ../sub1
251 251 revision 2ecb03bf44a94e749e8669481dd9069526ce7cb9
252 252 $ hg debugsub -R cloned/sub1
253 253 path sub2
254 254 source ../sub2
255 255 revision 53dd3430bcaf5ab4a7c48262bcad6d441f510487
256 256
257 257 Check that deep archiving works
258 258
259 259 $ cd cloned
260 260 $ echo 'test' > sub1/sub2/test.txt
261 261 $ hg --config extensions.largefiles=! add sub1/sub2/test.txt
262 262 $ mkdir sub1/sub2/folder
263 263 $ echo 'subfolder' > sub1/sub2/folder/test.txt
264 264 $ hg ci -ASm "add test.txt"
265 265 adding sub1/sub2/folder/test.txt
266 266 committing subrepository sub1
267 267 committing subrepository sub1/sub2
268 268
269 269 $ rm -r main
270 270 $ hg archive -S -qr 'wdir()' ../wdir
271 271 $ cat ../wdir/.hg_archival.txt
272 272 repo: 7f491f53a367861f47ee64a80eb997d1f341b77a
273 273 node: 9bb10eebee29dc0f1201dcf5977b811a540255fd+
274 274 branch: default
275 275 latesttag: null
276 276 latesttagdistance: 4
277 277 changessincelatesttag: 4
278 278 $ hg update -Cq .
279 279
280 280 A deleted subrepo file is flagged as dirty, like the top level repo
281 281
282 282 $ rm -r ../wdir sub1/sub2/folder/test.txt
283 283 $ hg archive -S -qr 'wdir()' ../wdir
284 284 $ cat ../wdir/.hg_archival.txt
285 285 repo: 7f491f53a367861f47ee64a80eb997d1f341b77a
286 286 node: 9bb10eebee29dc0f1201dcf5977b811a540255fd+
287 287 branch: default
288 288 latesttag: null
289 289 latesttagdistance: 4
290 290 changessincelatesttag: 4
291 291 $ hg update -Cq .
292 292 $ rm -r ../wdir
293 293
294 294 $ hg archive -S -qr 'wdir()' ../wdir \
295 295 > --config 'experimental.archivemetatemplate=archived {node|short}\n'
296 296 $ cat ../wdir/.hg_archival.txt
297 297 archived ffffffffffff
298 298 $ rm -r ../wdir
299 299
300 300 .. but first take a detour through some deep removal testing
301 301
302 302 $ hg remove -S -I 're:.*.txt' .
303 303 \r (no-eol) (esc)
304 304 searching [==========================================>] 1/1\r (no-eol) (esc)
305 305 searching [==========================================>] 1/1\r (no-eol) (esc)
306 306 \r (no-eol) (esc)
307 307 \r (no-eol) (esc)
308 308 deleting [=====================> ] 1/2\r (no-eol) (esc)
309 309 \r (no-eol) (esc)
310 310 \r (no-eol) (esc)
311 311 deleting [===========================================>] 2/2\r (no-eol) (esc)
312 312 \r (no-eol) (esc)
313 313 removing sub1/sub2/folder/test.txt
314 314 removing sub1/sub2/test.txt
315 315 $ hg status -S
316 316 R sub1/sub2/folder/test.txt
317 317 R sub1/sub2/test.txt
318 318 $ hg update -Cq
319 319 $ hg remove -I 're:.*.txt' sub1
320 320 \r (no-eol) (esc)
321 321 searching [==========================================>] 1/1\r (no-eol) (esc)
322 322 \r (no-eol) (esc)
323 323 \r (no-eol) (esc)
324 324 deleting [===========================================>] 1/1\r (no-eol) (esc)
325 325 \r (no-eol) (esc)
326 326 $ hg status -S
327 327 $ hg remove sub1/sub2/folder/test.txt
328 328 \r (no-eol) (esc)
329 329 searching [==========================================>] 1/1\r (no-eol) (esc)
330 330 searching [==========================================>] 1/1\r (no-eol) (esc)
331 331 \r (no-eol) (esc)
332 332 \r (no-eol) (esc)
333 333 deleting [===========================================>] 1/1\r (no-eol) (esc)
334 334 \r (no-eol) (esc)
335 335 \r (no-eol) (esc)
336 336 deleting [===========================================>] 1/1\r (no-eol) (esc)
337 337 \r (no-eol) (esc)
338 338 \r (no-eol) (esc)
339 339 deleting [===========================================>] 1/1\r (no-eol) (esc)
340 340 \r (no-eol) (esc)
341 341 $ hg remove sub1/.hgsubstate
342 342 \r (no-eol) (esc)
343 343 searching [==========================================>] 1/1\r (no-eol) (esc)
344 344 \r (no-eol) (esc)
345 345 \r (no-eol) (esc)
346 346 deleting [===========================================>] 1/1\r (no-eol) (esc)
347 347 \r (no-eol) (esc)
348 348 \r (no-eol) (esc)
349 349 deleting [===========================================>] 1/1\r (no-eol) (esc)
350 350 \r (no-eol) (esc)
351 351 $ mv sub1/.hgsub sub1/x.hgsub
352 352 $ hg status -S
353 353 warning: subrepo spec file 'sub1/.hgsub' not found
354 354 R sub1/.hgsubstate
355 355 R sub1/sub2/folder/test.txt
356 356 ! sub1/.hgsub
357 357 ? sub1/x.hgsub
358 $ hg status -R sub1
359 warning: subrepo spec file 'sub1/.hgsub' not found
360 R .hgsubstate
361 ! .hgsub
362 ? x.hgsub
358 363 $ mv sub1/x.hgsub sub1/.hgsub
359 364 $ hg update -Cq
360 365 $ touch sub1/foo
361 366 $ hg forget sub1/sub2/folder/test.txt
362 367 $ rm sub1/sub2/test.txt
363 368
364 369 Test relative path printing + subrepos
365 370 $ mkdir -p foo/bar
366 371 $ cd foo
367 372 $ touch bar/abc
368 373 $ hg addremove -S ..
369 374 \r (no-eol) (esc)
370 375 searching for exact renames [========================>] 1/1\r (no-eol) (esc)
371 376 \r (no-eol) (esc)
372 377 adding ../sub1/sub2/folder/test.txt
373 378 removing ../sub1/sub2/test.txt
374 379 adding ../sub1/foo
375 380 adding bar/abc
376 381 $ cd ..
377 382 $ hg status -S
378 383 A foo/bar/abc
379 384 A sub1/foo
380 385 R sub1/sub2/test.txt
381 386
382 387 Archive wdir() with subrepos
383 388 $ hg rm main
384 389 \r (no-eol) (esc)
385 390 deleting [===========================================>] 1/1\r (no-eol) (esc)
386 391 \r (no-eol) (esc)
387 392 $ hg archive -S -r 'wdir()' ../wdir
388 393 \r (no-eol) (esc)
389 394 archiving [ ] 0/3\r (no-eol) (esc)
390 395 archiving [=============> ] 1/3\r (no-eol) (esc)
391 396 archiving [===========================> ] 2/3\r (no-eol) (esc)
392 397 archiving [==========================================>] 3/3\r (no-eol) (esc)
393 398 \r (no-eol) (esc)
394 399 \r (no-eol) (esc)
395 400 archiving (sub1) [ ] 0/4\r (no-eol) (esc)
396 401 archiving (sub1) [========> ] 1/4\r (no-eol) (esc)
397 402 archiving (sub1) [=================> ] 2/4\r (no-eol) (esc)
398 403 archiving (sub1) [==========================> ] 3/4\r (no-eol) (esc)
399 404 archiving (sub1) [===================================>] 4/4\r (no-eol) (esc)
400 405 \r (no-eol) (esc)
401 406 \r (no-eol) (esc)
402 407 archiving (sub1/sub2) [ ] 0/2\r (no-eol) (esc)
403 408 archiving (sub1/sub2) [==============> ] 1/2\r (no-eol) (esc)
404 409 archiving (sub1/sub2) [==============================>] 2/2\r (no-eol) (esc)
405 410 \r (no-eol) (esc)
406 411 $ diff -r . ../wdir | egrep -v '\.hg$|^Common subdirectories:'
407 412 Only in ../wdir: .hg_archival.txt
408 413
409 414 $ find ../wdir -type f | sort
410 415 ../wdir/.hg_archival.txt
411 416 ../wdir/.hgsub
412 417 ../wdir/.hgsubstate
413 418 ../wdir/foo/bar/abc
414 419 ../wdir/sub1/.hgsub
415 420 ../wdir/sub1/.hgsubstate
416 421 ../wdir/sub1/foo
417 422 ../wdir/sub1/sub1
418 423 ../wdir/sub1/sub2/folder/test.txt
419 424 ../wdir/sub1/sub2/sub2
420 425
421 426 $ cat ../wdir/.hg_archival.txt
422 427 repo: 7f491f53a367861f47ee64a80eb997d1f341b77a
423 428 node: 9bb10eebee29dc0f1201dcf5977b811a540255fd+
424 429 branch: default
425 430 latesttag: null
426 431 latesttagdistance: 4
427 432 changessincelatesttag: 4
428 433
429 434 Attempting to archive 'wdir()' with a missing file is handled gracefully
430 435 $ rm sub1/sub1
431 436 $ rm -r ../wdir
432 437 $ hg archive -v -S -r 'wdir()' ../wdir
433 438 \r (no-eol) (esc)
434 439 archiving [ ] 0/3\r (no-eol) (esc)
435 440 archiving [=============> ] 1/3\r (no-eol) (esc)
436 441 archiving [===========================> ] 2/3\r (no-eol) (esc)
437 442 archiving [==========================================>] 3/3\r (no-eol) (esc)
438 443 \r (no-eol) (esc)
439 444 \r (no-eol) (esc)
440 445 archiving (sub1) [ ] 0/3\r (no-eol) (esc)
441 446 archiving (sub1) [===========> ] 1/3\r (no-eol) (esc)
442 447 archiving (sub1) [=======================> ] 2/3\r (no-eol) (esc)
443 448 archiving (sub1) [===================================>] 3/3\r (no-eol) (esc)
444 449 \r (no-eol) (esc)
445 450 \r (no-eol) (esc)
446 451 archiving (sub1/sub2) [ ] 0/2\r (no-eol) (esc)
447 452 archiving (sub1/sub2) [==============> ] 1/2\r (no-eol) (esc)
448 453 archiving (sub1/sub2) [==============================>] 2/2\r (no-eol) (esc)
449 454 \r (no-eol) (esc)
450 455 $ find ../wdir -type f | sort
451 456 ../wdir/.hg_archival.txt
452 457 ../wdir/.hgsub
453 458 ../wdir/.hgsubstate
454 459 ../wdir/foo/bar/abc
455 460 ../wdir/sub1/.hgsub
456 461 ../wdir/sub1/.hgsubstate
457 462 ../wdir/sub1/foo
458 463 ../wdir/sub1/sub2/folder/test.txt
459 464 ../wdir/sub1/sub2/sub2
460 465
461 466 Continue relative path printing + subrepos
462 467 $ hg update -Cq
463 468 $ rm -r ../wdir
464 469 $ hg archive -S -r 'wdir()' ../wdir
465 470 \r (no-eol) (esc)
466 471 archiving [ ] 0/3\r (no-eol) (esc)
467 472 archiving [=============> ] 1/3\r (no-eol) (esc)
468 473 archiving [===========================> ] 2/3\r (no-eol) (esc)
469 474 archiving [==========================================>] 3/3\r (no-eol) (esc)
470 475 \r (no-eol) (esc)
471 476 \r (no-eol) (esc)
472 477 archiving (sub1) [ ] 0/3\r (no-eol) (esc)
473 478 archiving (sub1) [===========> ] 1/3\r (no-eol) (esc)
474 479 archiving (sub1) [=======================> ] 2/3\r (no-eol) (esc)
475 480 archiving (sub1) [===================================>] 3/3\r (no-eol) (esc)
476 481 \r (no-eol) (esc)
477 482 \r (no-eol) (esc)
478 483 archiving (sub1/sub2) [ ] 0/3\r (no-eol) (esc)
479 484 archiving (sub1/sub2) [=========> ] 1/3\r (no-eol) (esc)
480 485 archiving (sub1/sub2) [===================> ] 2/3\r (no-eol) (esc)
481 486 archiving (sub1/sub2) [==============================>] 3/3\r (no-eol) (esc)
482 487 \r (no-eol) (esc)
483 488 $ cat ../wdir/.hg_archival.txt
484 489 repo: 7f491f53a367861f47ee64a80eb997d1f341b77a
485 490 node: 9bb10eebee29dc0f1201dcf5977b811a540255fd
486 491 branch: default
487 492 latesttag: null
488 493 latesttagdistance: 4
489 494 changessincelatesttag: 4
490 495
491 496 $ touch sub1/sub2/folder/bar
492 497 $ hg addremove sub1/sub2
493 498 adding sub1/sub2/folder/bar
494 499 $ hg status -S
495 500 A sub1/sub2/folder/bar
496 501 ? foo/bar/abc
497 502 ? sub1/foo
498 503 $ hg update -Cq
499 504 $ hg addremove sub1
500 505 adding sub1/sub2/folder/bar
501 506 adding sub1/foo
502 507 $ hg update -Cq
503 508 $ rm sub1/sub2/folder/test.txt
504 509 $ rm sub1/sub2/test.txt
505 510 $ hg ci -ASm "remove test.txt"
506 511 adding sub1/sub2/folder/bar
507 512 removing sub1/sub2/folder/test.txt
508 513 removing sub1/sub2/test.txt
509 514 adding sub1/foo
510 515 adding foo/bar/abc
511 516 committing subrepository sub1
512 517 committing subrepository sub1/sub2
513 518
514 519 $ hg forget sub1/sub2/sub2
515 520 $ echo x > sub1/sub2/x.txt
516 521 $ hg add sub1/sub2/x.txt
517 522
518 523 Files sees uncommitted adds and removes in subrepos
519 524 $ hg files -S
520 525 .hgsub
521 526 .hgsubstate
522 527 foo/bar/abc
523 528 main
524 529 sub1/.hgsub
525 530 sub1/.hgsubstate
526 531 sub1/foo
527 532 sub1/sub1
528 533 sub1/sub2/folder/bar
529 534 sub1/sub2/x.txt
530 535
531 536 $ hg files -S "set:eol('dos') or eol('unix') or size('<= 0')"
532 537 .hgsub
533 538 .hgsubstate
534 539 foo/bar/abc
535 540 main
536 541 sub1/.hgsub
537 542 sub1/.hgsubstate
538 543 sub1/foo
539 544 sub1/sub1
540 545 sub1/sub2/folder/bar
541 546 sub1/sub2/x.txt
542 547
543 548 $ hg files -r '.^' -S "set:eol('dos') or eol('unix')"
544 549 .hgsub
545 550 .hgsubstate
546 551 main
547 552 sub1/.hgsub
548 553 sub1/.hgsubstate
549 554 sub1/sub1
550 555 sub1/sub2/folder/test.txt
551 556 sub1/sub2/sub2
552 557 sub1/sub2/test.txt
553 558
554 559 $ hg files sub1
555 560 sub1/.hgsub
556 561 sub1/.hgsubstate
557 562 sub1/foo
558 563 sub1/sub1
559 564 sub1/sub2/folder/bar
560 565 sub1/sub2/x.txt
561 566
562 567 $ hg files sub1/sub2
563 568 sub1/sub2/folder/bar
564 569 sub1/sub2/x.txt
565 570
566 571 $ hg files
567 572 .hgsub
568 573 .hgsubstate
569 574 foo/bar/abc
570 575 main
571 576
572 577 $ hg files -S -r '.^' sub1/sub2/folder
573 578 sub1/sub2/folder/test.txt
574 579
575 580 $ hg files -S -r '.^' sub1/sub2/missing
576 581 sub1/sub2/missing: no such file in rev 78026e779ea6
577 582 [1]
578 583
579 584 $ hg files -r '.^' sub1/
580 585 sub1/.hgsub
581 586 sub1/.hgsubstate
582 587 sub1/sub1
583 588 sub1/sub2/folder/test.txt
584 589 sub1/sub2/sub2
585 590 sub1/sub2/test.txt
586 591
587 592 $ hg files -r '.^' sub1/sub2
588 593 sub1/sub2/folder/test.txt
589 594 sub1/sub2/sub2
590 595 sub1/sub2/test.txt
591 596
592 597 $ hg rollback -q
593 598 $ hg up -Cq
594 599
595 600 $ hg --config extensions.largefiles=! archive -S ../archive_all
596 601 \r (no-eol) (esc)
597 602 archiving [ ] 0/3\r (no-eol) (esc)
598 603 archiving [=============> ] 1/3\r (no-eol) (esc)
599 604 archiving [===========================> ] 2/3\r (no-eol) (esc)
600 605 archiving [==========================================>] 3/3\r (no-eol) (esc)
601 606 \r (no-eol) (esc)
602 607 \r (no-eol) (esc)
603 608 archiving (sub1) [ ] 0/3\r (no-eol) (esc)
604 609 archiving (sub1) [===========> ] 1/3\r (no-eol) (esc)
605 610 archiving (sub1) [=======================> ] 2/3\r (no-eol) (esc)
606 611 archiving (sub1) [===================================>] 3/3\r (no-eol) (esc)
607 612 \r (no-eol) (esc)
608 613 \r (no-eol) (esc)
609 614 archiving (sub1/sub2) [ ] 0/3\r (no-eol) (esc)
610 615 archiving (sub1/sub2) [=========> ] 1/3\r (no-eol) (esc)
611 616 archiving (sub1/sub2) [===================> ] 2/3\r (no-eol) (esc)
612 617 archiving (sub1/sub2) [==============================>] 3/3\r (no-eol) (esc)
613 618 \r (no-eol) (esc)
614 619 $ find ../archive_all | sort
615 620 ../archive_all
616 621 ../archive_all/.hg_archival.txt
617 622 ../archive_all/.hgsub
618 623 ../archive_all/.hgsubstate
619 624 ../archive_all/main
620 625 ../archive_all/sub1
621 626 ../archive_all/sub1/.hgsub
622 627 ../archive_all/sub1/.hgsubstate
623 628 ../archive_all/sub1/sub1
624 629 ../archive_all/sub1/sub2
625 630 ../archive_all/sub1/sub2/folder
626 631 ../archive_all/sub1/sub2/folder/test.txt
627 632 ../archive_all/sub1/sub2/sub2
628 633 ../archive_all/sub1/sub2/test.txt
629 634
630 635 Check that archive -X works in deep subrepos
631 636
632 637 $ hg --config extensions.largefiles=! archive -S -X '**test*' ../archive_exclude
633 638 \r (no-eol) (esc)
634 639 archiving [ ] 0/3\r (no-eol) (esc)
635 640 archiving [=============> ] 1/3\r (no-eol) (esc)
636 641 archiving [===========================> ] 2/3\r (no-eol) (esc)
637 642 archiving [==========================================>] 3/3\r (no-eol) (esc)
638 643 \r (no-eol) (esc)
639 644 \r (no-eol) (esc)
640 645 archiving (sub1) [ ] 0/3\r (no-eol) (esc)
641 646 archiving (sub1) [===========> ] 1/3\r (no-eol) (esc)
642 647 archiving (sub1) [=======================> ] 2/3\r (no-eol) (esc)
643 648 archiving (sub1) [===================================>] 3/3\r (no-eol) (esc)
644 649 \r (no-eol) (esc)
645 650 \r (no-eol) (esc)
646 651 archiving (sub1/sub2) [ ] 0/1\r (no-eol) (esc)
647 652 archiving (sub1/sub2) [==============================>] 1/1\r (no-eol) (esc)
648 653 \r (no-eol) (esc)
649 654 $ find ../archive_exclude | sort
650 655 ../archive_exclude
651 656 ../archive_exclude/.hg_archival.txt
652 657 ../archive_exclude/.hgsub
653 658 ../archive_exclude/.hgsubstate
654 659 ../archive_exclude/main
655 660 ../archive_exclude/sub1
656 661 ../archive_exclude/sub1/.hgsub
657 662 ../archive_exclude/sub1/.hgsubstate
658 663 ../archive_exclude/sub1/sub1
659 664 ../archive_exclude/sub1/sub2
660 665 ../archive_exclude/sub1/sub2/sub2
661 666
662 667 $ hg --config extensions.largefiles=! archive -S -I '**test*' ../archive_include
663 668 \r (no-eol) (esc)
664 669 archiving (sub1) [ <=> ] 0\r (no-eol) (esc)
665 670 \r (no-eol) (esc)
666 671 \r (no-eol) (esc)
667 672 archiving (sub1/sub2) [ ] 0/2\r (no-eol) (esc)
668 673 archiving (sub1/sub2) [==============> ] 1/2\r (no-eol) (esc)
669 674 archiving (sub1/sub2) [==============================>] 2/2\r (no-eol) (esc)
670 675 \r (no-eol) (esc)
671 676 $ find ../archive_include | sort
672 677 ../archive_include
673 678 ../archive_include/sub1
674 679 ../archive_include/sub1/sub2
675 680 ../archive_include/sub1/sub2/folder
676 681 ../archive_include/sub1/sub2/folder/test.txt
677 682 ../archive_include/sub1/sub2/test.txt
678 683
679 684 Check that deep archive works with largefiles (which overrides hgsubrepo impl)
680 685 This also tests the repo.ui regression in 43fb170a23bd, and that lf subrepo
681 686 subrepos are archived properly.
682 687 Note that add --large through a subrepo currently adds the file as a normal file
683 688
684 689 $ echo "large" > sub1/sub2/large.bin
685 690 $ hg --config extensions.largefiles= add --large -R sub1/sub2 sub1/sub2/large.bin
686 691 $ echo "large" > large.bin
687 692 $ hg --config extensions.largefiles= add --large large.bin
688 693 $ hg --config extensions.largefiles= ci -S -m "add large files"
689 694 committing subrepository sub1
690 695 committing subrepository sub1/sub2
691 696
692 697 $ hg --config extensions.largefiles= archive -S ../archive_lf
693 698 $ find ../archive_lf | sort
694 699 ../archive_lf
695 700 ../archive_lf/.hg_archival.txt
696 701 ../archive_lf/.hgsub
697 702 ../archive_lf/.hgsubstate
698 703 ../archive_lf/large.bin
699 704 ../archive_lf/main
700 705 ../archive_lf/sub1
701 706 ../archive_lf/sub1/.hgsub
702 707 ../archive_lf/sub1/.hgsubstate
703 708 ../archive_lf/sub1/sub1
704 709 ../archive_lf/sub1/sub2
705 710 ../archive_lf/sub1/sub2/folder
706 711 ../archive_lf/sub1/sub2/folder/test.txt
707 712 ../archive_lf/sub1/sub2/large.bin
708 713 ../archive_lf/sub1/sub2/sub2
709 714 ../archive_lf/sub1/sub2/test.txt
710 715 $ rm -rf ../archive_lf
711 716
712 717 Exclude large files from main and sub-sub repo
713 718
714 719 $ hg --config extensions.largefiles= archive -S -X '**.bin' ../archive_lf
715 720 $ find ../archive_lf | sort
716 721 ../archive_lf
717 722 ../archive_lf/.hg_archival.txt
718 723 ../archive_lf/.hgsub
719 724 ../archive_lf/.hgsubstate
720 725 ../archive_lf/main
721 726 ../archive_lf/sub1
722 727 ../archive_lf/sub1/.hgsub
723 728 ../archive_lf/sub1/.hgsubstate
724 729 ../archive_lf/sub1/sub1
725 730 ../archive_lf/sub1/sub2
726 731 ../archive_lf/sub1/sub2/folder
727 732 ../archive_lf/sub1/sub2/folder/test.txt
728 733 ../archive_lf/sub1/sub2/sub2
729 734 ../archive_lf/sub1/sub2/test.txt
730 735 $ rm -rf ../archive_lf
731 736
732 737 Exclude normal files from main and sub-sub repo
733 738
734 739 $ hg --config extensions.largefiles= archive -S -X '**.txt' -p '.' ../archive_lf.tgz
735 740 $ tar -tzf ../archive_lf.tgz | sort
736 741 .hgsub
737 742 .hgsubstate
738 743 large.bin
739 744 main
740 745 sub1/.hgsub
741 746 sub1/.hgsubstate
742 747 sub1/sub1
743 748 sub1/sub2/large.bin
744 749 sub1/sub2/sub2
745 750
746 751 Include normal files from within a largefiles subrepo
747 752
748 753 $ hg --config extensions.largefiles= archive -S -I '**.txt' ../archive_lf
749 754 $ find ../archive_lf | sort
750 755 ../archive_lf
751 756 ../archive_lf/.hg_archival.txt
752 757 ../archive_lf/sub1
753 758 ../archive_lf/sub1/sub2
754 759 ../archive_lf/sub1/sub2/folder
755 760 ../archive_lf/sub1/sub2/folder/test.txt
756 761 ../archive_lf/sub1/sub2/test.txt
757 762 $ rm -rf ../archive_lf
758 763
759 764 Include large files from within a largefiles subrepo
760 765
761 766 $ hg --config extensions.largefiles= archive -S -I '**.bin' ../archive_lf
762 767 $ find ../archive_lf | sort
763 768 ../archive_lf
764 769 ../archive_lf/large.bin
765 770 ../archive_lf/sub1
766 771 ../archive_lf/sub1/sub2
767 772 ../archive_lf/sub1/sub2/large.bin
768 773 $ rm -rf ../archive_lf
769 774
770 775 Find an exact largefile match in a largefiles subrepo
771 776
772 777 $ hg --config extensions.largefiles= archive -S -I 'sub1/sub2/large.bin' ../archive_lf
773 778 $ find ../archive_lf | sort
774 779 ../archive_lf
775 780 ../archive_lf/sub1
776 781 ../archive_lf/sub1/sub2
777 782 ../archive_lf/sub1/sub2/large.bin
778 783 $ rm -rf ../archive_lf
779 784
780 785 The local repo enables largefiles if a largefiles repo is cloned
781 786
782 787 $ hg showconfig extensions
783 788 extensions.largefiles=
784 789
785 790 $ hg --config extensions.largefiles= clone -qU . ../lfclone
786 791 $ grep largefiles ../lfclone/.hg/requires
787 792 largefiles
788 793
789 794 Find an exact match to a standin (should archive nothing)
790 795 $ hg --config extensions.largefiles= archive -S -I 'sub/sub2/.hglf/large.bin' ../archive_lf
791 796 $ find ../archive_lf 2> /dev/null | sort
792 797
793 798 $ cat >> $HGRCPATH <<EOF
794 799 > [extensions]
795 800 > largefiles=
796 801 > [largefiles]
797 802 > patterns=glob:**.dat
798 803 > EOF
799 804
800 805 Test forget through a deep subrepo with the largefiles extension, both a
801 806 largefile and a normal file. Then a largefile that hasn't been committed yet.
802 807 $ touch sub1/sub2/untracked.txt
803 808 $ touch sub1/sub2/large.dat
804 809 $ hg forget sub1/sub2/large.bin sub1/sub2/test.txt sub1/sub2/untracked.txt
805 810 not removing sub1/sub2/untracked.txt: file is already untracked
806 811 [1]
807 812 $ hg add --large --dry-run -v sub1/sub2/untracked.txt
808 813 adding sub1/sub2/untracked.txt as a largefile
809 814 $ hg add --large -v sub1/sub2/untracked.txt
810 815 adding sub1/sub2/untracked.txt as a largefile
811 816 $ hg add --normal -v sub1/sub2/large.dat
812 817 adding sub1/sub2/large.dat
813 818 $ hg forget -v sub1/sub2/untracked.txt
814 819 removing sub1/sub2/untracked.txt
815 820 $ hg status -S
816 821 A sub1/sub2/large.dat
817 822 R sub1/sub2/large.bin
818 823 R sub1/sub2/test.txt
819 824 ? foo/bar/abc
820 825 ? sub1/sub2/untracked.txt
821 826 ? sub1/sub2/x.txt
822 827 $ hg add sub1/sub2
823 828
824 829 $ hg archive -S -r 'wdir()' ../wdir2
825 830 $ diff -r . ../wdir2 | egrep -v '\.hg$|^Common subdirectories:'
826 831 Only in ../wdir2: .hg_archival.txt
827 832 Only in .: .hglf
828 833 Only in .: foo
829 834 Only in ./sub1/sub2: large.bin
830 835 Only in ./sub1/sub2: test.txt
831 836 Only in ./sub1/sub2: untracked.txt
832 837 Only in ./sub1/sub2: x.txt
833 838 $ find ../wdir2 -type f | sort
834 839 ../wdir2/.hg_archival.txt
835 840 ../wdir2/.hgsub
836 841 ../wdir2/.hgsubstate
837 842 ../wdir2/large.bin
838 843 ../wdir2/main
839 844 ../wdir2/sub1/.hgsub
840 845 ../wdir2/sub1/.hgsubstate
841 846 ../wdir2/sub1/sub1
842 847 ../wdir2/sub1/sub2/folder/test.txt
843 848 ../wdir2/sub1/sub2/large.dat
844 849 ../wdir2/sub1/sub2/sub2
845 850 $ hg status -S -mac -n | sort
846 851 .hgsub
847 852 .hgsubstate
848 853 large.bin
849 854 main
850 855 sub1/.hgsub
851 856 sub1/.hgsubstate
852 857 sub1/sub1
853 858 sub1/sub2/folder/test.txt
854 859 sub1/sub2/large.dat
855 860 sub1/sub2/sub2
856 861
857 862 $ hg ci -Sqm 'forget testing'
858 863
859 864 Test 'wdir()' modified file archiving with largefiles
860 865 $ echo 'mod' > main
861 866 $ echo 'mod' > large.bin
862 867 $ echo 'mod' > sub1/sub2/large.dat
863 868 $ hg archive -S -r 'wdir()' ../wdir3
864 869 $ diff -r . ../wdir3 | egrep -v '\.hg$|^Common subdirectories'
865 870 Only in ../wdir3: .hg_archival.txt
866 871 Only in .: .hglf
867 872 Only in .: foo
868 873 Only in ./sub1/sub2: large.bin
869 874 Only in ./sub1/sub2: test.txt
870 875 Only in ./sub1/sub2: untracked.txt
871 876 Only in ./sub1/sub2: x.txt
872 877 $ find ../wdir3 -type f | sort
873 878 ../wdir3/.hg_archival.txt
874 879 ../wdir3/.hgsub
875 880 ../wdir3/.hgsubstate
876 881 ../wdir3/large.bin
877 882 ../wdir3/main
878 883 ../wdir3/sub1/.hgsub
879 884 ../wdir3/sub1/.hgsubstate
880 885 ../wdir3/sub1/sub1
881 886 ../wdir3/sub1/sub2/folder/test.txt
882 887 ../wdir3/sub1/sub2/large.dat
883 888 ../wdir3/sub1/sub2/sub2
884 889 $ hg up -Cq
885 890
886 891 Test issue4330: commit a directory where only normal files have changed
887 892 $ touch foo/bar/large.dat
888 893 $ hg add --large foo/bar/large.dat
889 894 $ hg ci -m 'add foo/bar/large.dat'
890 895 $ touch a.txt
891 896 $ touch a.dat
892 897 $ hg add -v foo/bar/abc a.txt a.dat
893 898 adding a.dat as a largefile
894 899 adding a.txt
895 900 adding foo/bar/abc
896 901 $ hg ci -m 'dir commit with only normal file deltas' foo/bar
897 902 $ hg status
898 903 A a.dat
899 904 A a.txt
900 905
901 906 Test a directory commit with a changed largefile and a changed normal file
902 907 $ echo changed > foo/bar/large.dat
903 908 $ echo changed > foo/bar/abc
904 909 $ hg ci -m 'dir commit with normal and lf file deltas' foo
905 910 $ hg status
906 911 A a.dat
907 912 A a.txt
908 913
909 914 $ hg ci -m "add a.*"
910 915 $ hg mv a.dat b.dat
911 916 $ hg mv foo/bar/abc foo/bar/def
912 917 $ hg status -C
913 918 A b.dat
914 919 a.dat
915 920 A foo/bar/def
916 921 foo/bar/abc
917 922 R a.dat
918 923 R foo/bar/abc
919 924
920 925 $ hg ci -m "move large and normal"
921 926 $ hg status -C --rev '.^' --rev .
922 927 A b.dat
923 928 a.dat
924 929 A foo/bar/def
925 930 foo/bar/abc
926 931 R a.dat
927 932 R foo/bar/abc
928 933
929 934
930 935 $ echo foo > main
931 936 $ hg ci -m "mod parent only"
932 937 $ hg init sub3
933 938 $ echo "sub3 = sub3" >> .hgsub
934 939 $ echo xyz > sub3/a.txt
935 940 $ hg add sub3/a.txt
936 941 $ hg ci -Sm "add sub3"
937 942 committing subrepository sub3
938 943 $ cat .hgsub | grep -v sub3 > .hgsub1
939 944 $ mv .hgsub1 .hgsub
940 945 $ hg ci -m "remove sub3"
941 946
942 947 $ hg log -r "subrepo()" --style compact
943 948 0 7f491f53a367 1970-01-01 00:00 +0000 test
944 949 main import
945 950
946 951 1 ffe6649062fe 1970-01-01 00:00 +0000 test
947 952 deep nested modif should trigger a commit
948 953
949 954 2 9bb10eebee29 1970-01-01 00:00 +0000 test
950 955 add test.txt
951 956
952 957 3 7c64f035294f 1970-01-01 00:00 +0000 test
953 958 add large files
954 959
955 960 4 f734a59e2e35 1970-01-01 00:00 +0000 test
956 961 forget testing
957 962
958 963 11 9685a22af5db 1970-01-01 00:00 +0000 test
959 964 add sub3
960 965
961 966 12[tip] 2e0485b475b9 1970-01-01 00:00 +0000 test
962 967 remove sub3
963 968
964 969 $ hg log -r "subrepo('sub3')" --style compact
965 970 11 9685a22af5db 1970-01-01 00:00 +0000 test
966 971 add sub3
967 972
968 973 12[tip] 2e0485b475b9 1970-01-01 00:00 +0000 test
969 974 remove sub3
970 975
971 976 $ hg log -r "subrepo('bogus')" --style compact
972 977
973 978
974 979 Test .hgsubstate in the R state
975 980
976 981 $ hg rm .hgsub .hgsubstate
977 982 \r (no-eol) (esc)
978 983 deleting [=====================> ] 1/2\r (no-eol) (esc)
979 984 deleting [===========================================>] 2/2\r (no-eol) (esc)
980 985 \r (no-eol) (esc)
981 986 $ hg ci -m 'trash subrepo tracking'
982 987
983 988 $ hg log -r "subrepo('re:sub\d+')" --style compact
984 989 0 7f491f53a367 1970-01-01 00:00 +0000 test
985 990 main import
986 991
987 992 1 ffe6649062fe 1970-01-01 00:00 +0000 test
988 993 deep nested modif should trigger a commit
989 994
990 995 2 9bb10eebee29 1970-01-01 00:00 +0000 test
991 996 add test.txt
992 997
993 998 3 7c64f035294f 1970-01-01 00:00 +0000 test
994 999 add large files
995 1000
996 1001 4 f734a59e2e35 1970-01-01 00:00 +0000 test
997 1002 forget testing
998 1003
999 1004 11 9685a22af5db 1970-01-01 00:00 +0000 test
1000 1005 add sub3
1001 1006
1002 1007 12 2e0485b475b9 1970-01-01 00:00 +0000 test
1003 1008 remove sub3
1004 1009
1005 1010 13[tip] a68b2c361653 1970-01-01 00:00 +0000 test
1006 1011 trash subrepo tracking
1007 1012
1008 1013
1009 1014 Restore the trashed subrepo tracking
1010 1015
1011 1016 $ hg rollback -q
1012 1017 $ hg update -Cq .
1013 1018
1014 1019 Interaction with extdiff, largefiles and subrepos
1015 1020
1016 1021 $ hg --config extensions.extdiff= pdiff -S
1017 1022
1018 1023 $ hg --config extensions.extdiff= pdiff -r '.^' -S
1019 1024 \r (no-eol) (esc)
1020 1025 archiving [ ] 0/2\r (no-eol) (esc)
1021 1026 archiving [====================> ] 1/2\r (no-eol) (esc)
1022 1027 archiving [==========================================>] 2/2\r (no-eol) (esc)
1023 1028 \r (no-eol) (esc)
1024 1029 \r (no-eol) (esc)
1025 1030 archiving (sub1) [ <=> ] 0\r (no-eol) (esc)
1026 1031 \r (no-eol) (esc)
1027 1032 \r (no-eol) (esc)
1028 1033 archiving (sub1/sub2) [ <=> ] 0\r (no-eol) (esc)
1029 1034 \r (no-eol) (esc)
1030 1035 \r (no-eol) (esc)
1031 1036 archiving (sub3) [ <=> ] 0\r (no-eol) (esc)
1032 1037 \r (no-eol) (esc)
1033 1038 \r (no-eol) (esc)
1034 1039 archiving [ ] 0/2\r (no-eol) (esc)
1035 1040 archiving [====================> ] 1/2\r (no-eol) (esc)
1036 1041 archiving [==========================================>] 2/2\r (no-eol) (esc)
1037 1042 \r (no-eol) (esc)
1038 1043 \r (no-eol) (esc)
1039 1044 archiving (sub1) [ <=> ] 0\r (no-eol) (esc)
1040 1045 \r (no-eol) (esc)
1041 1046 \r (no-eol) (esc)
1042 1047 archiving (sub1/sub2) [ <=> ] 0\r (no-eol) (esc)
1043 1048 \r (no-eol) (esc)
1044 1049 diff -Nru cloned.*/.hgsub cloned/.hgsub (glob)
1045 1050 --- cloned.*/.hgsub * (glob)
1046 1051 +++ cloned/.hgsub * (glob)
1047 1052 @@ -1,2 +1* @@ (glob)
1048 1053 sub1 = ../sub1
1049 1054 -sub3 = sub3
1050 1055 diff -Nru cloned.*/.hgsubstate cloned/.hgsubstate (glob)
1051 1056 --- cloned.*/.hgsubstate * (glob)
1052 1057 +++ cloned/.hgsubstate * (glob)
1053 1058 @@ -1,2 +1* @@ (glob)
1054 1059 7a36fa02b66e61f27f3d4a822809f159479b8ab2 sub1
1055 1060 -b1a26de6f2a045a9f079323693614ee322f1ff7e sub3
1056 1061 [1]
1057 1062
1058 1063 $ hg --config extensions.extdiff= pdiff -r 0 -r '.^' -S
1059 1064 \r (no-eol) (esc)
1060 1065 archiving [ ] 0/3\r (no-eol) (esc)
1061 1066 archiving [=============> ] 1/3\r (no-eol) (esc)
1062 1067 archiving [===========================> ] 2/3\r (no-eol) (esc)
1063 1068 archiving [==========================================>] 3/3\r (no-eol) (esc)
1064 1069 \r (no-eol) (esc)
1065 1070 \r (no-eol) (esc)
1066 1071 archiving (sub1) [ ] 0/1\r (no-eol) (esc)
1067 1072 archiving (sub1) [===================================>] 1/1\r (no-eol) (esc)
1068 1073 \r (no-eol) (esc)
1069 1074 \r (no-eol) (esc)
1070 1075 archiving (sub1/sub2) [ ] 0/1\r (no-eol) (esc)
1071 1076 archiving (sub1/sub2) [==============================>] 1/1\r (no-eol) (esc)
1072 1077 \r (no-eol) (esc)
1073 1078 \r (no-eol) (esc)
1074 1079 archiving [ ] 0/8\r (no-eol) (esc)
1075 1080 archiving [====> ] 1/8\r (no-eol) (esc)
1076 1081 archiving [=========> ] 2/8\r (no-eol) (esc)
1077 1082 archiving [===============> ] 3/8\r (no-eol) (esc)
1078 1083 archiving [====================> ] 4/8\r (no-eol) (esc)
1079 1084 archiving [=========================> ] 5/8\r (no-eol) (esc)
1080 1085 archiving [===============================> ] 6/8\r (no-eol) (esc)
1081 1086 archiving [====================================> ] 7/8\r (no-eol) (esc)
1082 1087 archiving [==========================================>] 8/8\r (no-eol) (esc)
1083 1088 \r (no-eol) (esc)
1084 1089 \r (no-eol) (esc)
1085 1090 archiving (sub1) [ ] 0/1\r (no-eol) (esc)
1086 1091 archiving (sub1) [===================================>] 1/1\r (no-eol) (esc)
1087 1092 \r (no-eol) (esc)
1088 1093 \r (no-eol) (esc)
1089 1094 archiving (sub1/sub2) [ ] 0/3\r (no-eol) (esc)
1090 1095 archiving (sub1/sub2) [=========> ] 1/3\r (no-eol) (esc)
1091 1096 archiving (sub1/sub2) [===================> ] 2/3\r (no-eol) (esc)
1092 1097 archiving (sub1/sub2) [==============================>] 3/3\r (no-eol) (esc)
1093 1098 \r (no-eol) (esc)
1094 1099 \r (no-eol) (esc)
1095 1100 archiving (sub3) [ ] 0/1\r (no-eol) (esc)
1096 1101 archiving (sub3) [===================================>] 1/1\r (no-eol) (esc)
1097 1102 \r (no-eol) (esc)
1098 1103 diff -Nru cloned.*/.hglf/b.dat cloned.*/.hglf/b.dat (glob)
1099 1104 --- cloned.*/.hglf/b.dat * (glob)
1100 1105 +++ cloned.*/.hglf/b.dat * (glob)
1101 1106 @@ -*,0 +1* @@ (glob)
1102 1107 +da39a3ee5e6b4b0d3255bfef95601890afd80709
1103 1108 diff -Nru cloned.*/.hglf/foo/bar/large.dat cloned.*/.hglf/foo/bar/large.dat (glob)
1104 1109 --- cloned.*/.hglf/foo/bar/large.dat * (glob)
1105 1110 +++ cloned.*/.hglf/foo/bar/large.dat * (glob)
1106 1111 @@ -*,0 +1* @@ (glob)
1107 1112 +2f6933b5ee0f5fdd823d9717d8729f3c2523811b
1108 1113 diff -Nru cloned.*/.hglf/large.bin cloned.*/.hglf/large.bin (glob)
1109 1114 --- cloned.*/.hglf/large.bin * (glob)
1110 1115 +++ cloned.*/.hglf/large.bin * (glob)
1111 1116 @@ -*,0 +1* @@ (glob)
1112 1117 +7f7097b041ccf68cc5561e9600da4655d21c6d18
1113 1118 diff -Nru cloned.*/.hgsub cloned.*/.hgsub (glob)
1114 1119 --- cloned.*/.hgsub * (glob)
1115 1120 +++ cloned.*/.hgsub * (glob)
1116 1121 @@ -1* +1,2 @@ (glob)
1117 1122 sub1 = ../sub1
1118 1123 +sub3 = sub3
1119 1124 diff -Nru cloned.*/.hgsubstate cloned.*/.hgsubstate (glob)
1120 1125 --- cloned.*/.hgsubstate * (glob)
1121 1126 +++ cloned.*/.hgsubstate * (glob)
1122 1127 @@ -1* +1,2 @@ (glob)
1123 1128 -fc3b4ce2696f7741438c79207583768f2ce6b0dd sub1
1124 1129 +7a36fa02b66e61f27f3d4a822809f159479b8ab2 sub1
1125 1130 +b1a26de6f2a045a9f079323693614ee322f1ff7e sub3
1126 1131 diff -Nru cloned.*/foo/bar/def cloned.*/foo/bar/def (glob)
1127 1132 --- cloned.*/foo/bar/def * (glob)
1128 1133 +++ cloned.*/foo/bar/def * (glob)
1129 1134 @@ -*,0 +1* @@ (glob)
1130 1135 +changed
1131 1136 diff -Nru cloned.*/main cloned.*/main (glob)
1132 1137 --- cloned.*/main * (glob)
1133 1138 +++ cloned.*/main * (glob)
1134 1139 @@ -1* +1* @@ (glob)
1135 1140 -main
1136 1141 +foo
1137 1142 diff -Nru cloned.*/sub1/.hgsubstate cloned.*/sub1/.hgsubstate (glob)
1138 1143 --- cloned.*/sub1/.hgsubstate * (glob)
1139 1144 +++ cloned.*/sub1/.hgsubstate * (glob)
1140 1145 @@ -1* +1* @@ (glob)
1141 1146 -c57a0840e3badd667ef3c3ef65471609acb2ba3c sub2
1142 1147 +c77908c81ccea3794a896c79e98b0e004aee2e9e sub2
1143 1148 diff -Nru cloned.*/sub1/sub2/folder/test.txt cloned.*/sub1/sub2/folder/test.txt (glob)
1144 1149 --- cloned.*/sub1/sub2/folder/test.txt * (glob)
1145 1150 +++ cloned.*/sub1/sub2/folder/test.txt * (glob)
1146 1151 @@ -*,0 +1* @@ (glob)
1147 1152 +subfolder
1148 1153 diff -Nru cloned.*/sub1/sub2/sub2 cloned.*/sub1/sub2/sub2 (glob)
1149 1154 --- cloned.*/sub1/sub2/sub2 * (glob)
1150 1155 +++ cloned.*/sub1/sub2/sub2 * (glob)
1151 1156 @@ -1* +1* @@ (glob)
1152 1157 -sub2
1153 1158 +modified
1154 1159 diff -Nru cloned.*/sub3/a.txt cloned.*/sub3/a.txt (glob)
1155 1160 --- cloned.*/sub3/a.txt * (glob)
1156 1161 +++ cloned.*/sub3/a.txt * (glob)
1157 1162 @@ -*,0 +1* @@ (glob)
1158 1163 +xyz
1159 1164 [1]
1160 1165
1161 1166 $ echo mod > sub1/sub2/sub2
1162 1167 $ hg --config extensions.extdiff= pdiff -S
1163 1168 \r (no-eol) (esc)
1164 1169 archiving (sub1) [ <=> ] 0\r (no-eol) (esc)
1165 1170 \r (no-eol) (esc)
1166 1171 \r (no-eol) (esc)
1167 1172 archiving (sub1/sub2) [ ] 0/1\r (no-eol) (esc)
1168 1173 archiving (sub1/sub2) [==============================>] 1/1\r (no-eol) (esc)
1169 1174 \r (no-eol) (esc)
1170 1175 --- */cloned.*/sub1/sub2/sub2 * (glob)
1171 1176 +++ */cloned/sub1/sub2/sub2 * (glob)
1172 1177 @@ -1* +1* @@ (glob)
1173 1178 -modified
1174 1179 +mod
1175 1180 [1]
1176 1181
1177 1182 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now