##// END OF EJS Templates
match: write forceincludematcher using unionmatcher...
Martin von Zweigbergk -
r33447:6f4e5e59 default
parent child Browse files
Show More
@@ -1,1027 +1,1013
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
9 9
10 10 import copy
11 11 import os
12 12 import re
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 error,
17 17 pathutil,
18 18 util,
19 19 )
20 20
21 21 propertycache = util.propertycache
22 22
23 23 def _rematcher(regex):
24 24 '''compile the regexp with the best available regexp engine and return a
25 25 matcher function'''
26 26 m = util.re.compile(regex)
27 27 try:
28 28 # slightly faster, provided by facebook's re2 bindings
29 29 return m.test_match
30 30 except AttributeError:
31 31 return m.match
32 32
33 33 def _expandsets(kindpats, ctx, listsubrepos):
34 34 '''Returns the kindpats list with the 'set' patterns expanded.'''
35 35 fset = set()
36 36 other = []
37 37
38 38 for kind, pat, source in kindpats:
39 39 if kind == 'set':
40 40 if not ctx:
41 41 raise error.ProgrammingError("fileset expression with no "
42 42 "context")
43 43 s = ctx.getfileset(pat)
44 44 fset.update(s)
45 45
46 46 if listsubrepos:
47 47 for subpath in ctx.substate:
48 48 s = ctx.sub(subpath).getfileset(pat)
49 49 fset.update(subpath + '/' + f for f in s)
50 50
51 51 continue
52 52 other.append((kind, pat, source))
53 53 return fset, other
54 54
55 55 def _expandsubinclude(kindpats, root):
56 56 '''Returns the list of subinclude matcher args and the kindpats without the
57 57 subincludes in it.'''
58 58 relmatchers = []
59 59 other = []
60 60
61 61 for kind, pat, source in kindpats:
62 62 if kind == 'subinclude':
63 63 sourceroot = pathutil.dirname(util.normpath(source))
64 64 pat = util.pconvert(pat)
65 65 path = pathutil.join(sourceroot, pat)
66 66
67 67 newroot = pathutil.dirname(path)
68 68 matcherargs = (newroot, '', [], ['include:%s' % path])
69 69
70 70 prefix = pathutil.canonpath(root, root, newroot)
71 71 if prefix:
72 72 prefix += '/'
73 73 relmatchers.append((prefix, matcherargs))
74 74 else:
75 75 other.append((kind, pat, source))
76 76
77 77 return relmatchers, other
78 78
79 79 def _kindpatsalwaysmatch(kindpats):
80 80 """"Checks whether the kindspats match everything, as e.g.
81 81 'relpath:.' does.
82 82 """
83 83 for kind, pat, source in kindpats:
84 84 if pat != '' or kind not in ['relpath', 'glob']:
85 85 return False
86 86 return True
87 87
88 88 def match(root, cwd, patterns=None, include=None, exclude=None, default='glob',
89 89 exact=False, auditor=None, ctx=None, listsubrepos=False, warn=None,
90 90 badfn=None, icasefs=False):
91 91 """build an object to match a set of file patterns
92 92
93 93 arguments:
94 94 root - the canonical root of the tree you're matching against
95 95 cwd - the current working directory, if relevant
96 96 patterns - patterns to find
97 97 include - patterns to include (unless they are excluded)
98 98 exclude - patterns to exclude (even if they are included)
99 99 default - if a pattern in patterns has no explicit type, assume this one
100 100 exact - patterns are actually filenames (include/exclude still apply)
101 101 warn - optional function used for printing warnings
102 102 badfn - optional bad() callback for this matcher instead of the default
103 103 icasefs - make a matcher for wdir on case insensitive filesystems, which
104 104 normalizes the given patterns to the case in the filesystem
105 105
106 106 a pattern is one of:
107 107 'glob:<glob>' - a glob relative to cwd
108 108 're:<regexp>' - a regular expression
109 109 'path:<path>' - a path relative to repository root, which is matched
110 110 recursively
111 111 'rootfilesin:<path>' - a path relative to repository root, which is
112 112 matched non-recursively (will not match subdirectories)
113 113 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
114 114 'relpath:<path>' - a path relative to cwd
115 115 'relre:<regexp>' - a regexp that needn't match the start of a name
116 116 'set:<fileset>' - a fileset expression
117 117 'include:<path>' - a file of patterns to read and include
118 118 'subinclude:<path>' - a file of patterns to match against files under
119 119 the same directory
120 120 '<something>' - a pattern of the specified default type
121 121 """
122 122 normalize = _donormalize
123 123 if icasefs:
124 124 if exact:
125 125 raise error.ProgrammingError("a case-insensitive exact matcher "
126 126 "doesn't make sense")
127 127 dirstate = ctx.repo().dirstate
128 128 dsnormalize = dirstate.normalize
129 129
130 130 def normalize(patterns, default, root, cwd, auditor, warn):
131 131 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
132 132 kindpats = []
133 133 for kind, pats, source in kp:
134 134 if kind not in ('re', 'relre'): # regex can't be normalized
135 135 p = pats
136 136 pats = dsnormalize(pats)
137 137
138 138 # Preserve the original to handle a case only rename.
139 139 if p != pats and p in dirstate:
140 140 kindpats.append((kind, p, source))
141 141
142 142 kindpats.append((kind, pats, source))
143 143 return kindpats
144 144
145 145 if exact:
146 146 m = exactmatcher(root, cwd, patterns, badfn)
147 147 elif patterns:
148 148 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
149 149 if _kindpatsalwaysmatch(kindpats):
150 150 m = alwaysmatcher(root, cwd, badfn, relativeuipath=True)
151 151 else:
152 152 m = patternmatcher(root, cwd, kindpats, ctx=ctx,
153 153 listsubrepos=listsubrepos, badfn=badfn)
154 154 else:
155 155 # It's a little strange that no patterns means to match everything.
156 156 # Consider changing this to match nothing (probably using nevermatcher).
157 157 m = alwaysmatcher(root, cwd, badfn)
158 158
159 159 if include:
160 160 kindpats = normalize(include, 'glob', root, cwd, auditor, warn)
161 161 im = includematcher(root, cwd, kindpats, ctx=ctx,
162 162 listsubrepos=listsubrepos, badfn=None)
163 163 m = intersectmatchers(m, im)
164 164 if exclude:
165 165 kindpats = normalize(exclude, 'glob', root, cwd, auditor, warn)
166 166 em = includematcher(root, cwd, kindpats, ctx=ctx,
167 167 listsubrepos=listsubrepos, badfn=None)
168 168 m = differencematcher(m, em)
169 169 return m
170 170
171 171 def exact(root, cwd, files, badfn=None):
172 172 return exactmatcher(root, cwd, files, badfn=badfn)
173 173
174 174 def always(root, cwd):
175 175 return alwaysmatcher(root, cwd)
176 176
177 177 def never(root, cwd):
178 178 return nevermatcher(root, cwd)
179 179
180 180 def badmatch(match, badfn):
181 181 """Make a copy of the given matcher, replacing its bad method with the given
182 182 one.
183 183 """
184 184 m = copy.copy(match)
185 185 m.bad = badfn
186 186 return m
187 187
188 188 def _donormalize(patterns, default, root, cwd, auditor, warn):
189 189 '''Convert 'kind:pat' from the patterns list to tuples with kind and
190 190 normalized and rooted patterns and with listfiles expanded.'''
191 191 kindpats = []
192 192 for kind, pat in [_patsplit(p, default) for p in patterns]:
193 193 if kind in ('glob', 'relpath'):
194 194 pat = pathutil.canonpath(root, cwd, pat, auditor)
195 195 elif kind in ('relglob', 'path', 'rootfilesin'):
196 196 pat = util.normpath(pat)
197 197 elif kind in ('listfile', 'listfile0'):
198 198 try:
199 199 files = util.readfile(pat)
200 200 if kind == 'listfile0':
201 201 files = files.split('\0')
202 202 else:
203 203 files = files.splitlines()
204 204 files = [f for f in files if f]
205 205 except EnvironmentError:
206 206 raise error.Abort(_("unable to read file list (%s)") % pat)
207 207 for k, p, source in _donormalize(files, default, root, cwd,
208 208 auditor, warn):
209 209 kindpats.append((k, p, pat))
210 210 continue
211 211 elif kind == 'include':
212 212 try:
213 213 fullpath = os.path.join(root, util.localpath(pat))
214 214 includepats = readpatternfile(fullpath, warn)
215 215 for k, p, source in _donormalize(includepats, default,
216 216 root, cwd, auditor, warn):
217 217 kindpats.append((k, p, source or pat))
218 218 except error.Abort as inst:
219 219 raise error.Abort('%s: %s' % (pat, inst[0]))
220 220 except IOError as inst:
221 221 if warn:
222 222 warn(_("skipping unreadable pattern file '%s': %s\n") %
223 223 (pat, inst.strerror))
224 224 continue
225 225 # else: re or relre - which cannot be normalized
226 226 kindpats.append((kind, pat, ''))
227 227 return kindpats
228 228
229 229 class basematcher(object):
230 230
231 231 def __init__(self, root, cwd, badfn=None, relativeuipath=True):
232 232 self._root = root
233 233 self._cwd = cwd
234 234 if badfn is not None:
235 235 self.bad = badfn
236 236 self._relativeuipath = relativeuipath
237 237
238 238 def __call__(self, fn):
239 239 return self.matchfn(fn)
240 240 def __iter__(self):
241 241 for f in self._files:
242 242 yield f
243 243 # Callbacks related to how the matcher is used by dirstate.walk.
244 244 # Subscribers to these events must monkeypatch the matcher object.
245 245 def bad(self, f, msg):
246 246 '''Callback from dirstate.walk for each explicit file that can't be
247 247 found/accessed, with an error message.'''
248 248 pass
249 249
250 250 # If an explicitdir is set, it will be called when an explicitly listed
251 251 # directory is visited.
252 252 explicitdir = None
253 253
254 254 # If an traversedir is set, it will be called when a directory discovered
255 255 # by recursive traversal is visited.
256 256 traversedir = None
257 257
258 258 def abs(self, f):
259 259 '''Convert a repo path back to path that is relative to the root of the
260 260 matcher.'''
261 261 return f
262 262
263 263 def rel(self, f):
264 264 '''Convert repo path back to path that is relative to cwd of matcher.'''
265 265 return util.pathto(self._root, self._cwd, f)
266 266
267 267 def uipath(self, f):
268 268 '''Convert repo path to a display path. If patterns or -I/-X were used
269 269 to create this matcher, the display path will be relative to cwd.
270 270 Otherwise it is relative to the root of the repo.'''
271 271 return (self._relativeuipath and self.rel(f)) or self.abs(f)
272 272
273 273 @propertycache
274 274 def _files(self):
275 275 return []
276 276
277 277 def files(self):
278 278 '''Explicitly listed files or patterns or roots:
279 279 if no patterns or .always(): empty list,
280 280 if exact: list exact files,
281 281 if not .anypats(): list all files and dirs,
282 282 else: optimal roots'''
283 283 return self._files
284 284
285 285 @propertycache
286 286 def _fileset(self):
287 287 return set(self._files)
288 288
289 289 def exact(self, f):
290 290 '''Returns True if f is in .files().'''
291 291 return f in self._fileset
292 292
293 293 def matchfn(self, f):
294 294 return False
295 295
296 296 def visitdir(self, dir):
297 297 '''Decides whether a directory should be visited based on whether it
298 298 has potential matches in it or one of its subdirectories. This is
299 299 based on the match's primary, included, and excluded patterns.
300 300
301 301 Returns the string 'all' if the given directory and all subdirectories
302 302 should be visited. Otherwise returns True or False indicating whether
303 303 the given directory should be visited.
304 304
305 305 This function's behavior is undefined if it has returned False for
306 306 one of the dir's parent directories.
307 307 '''
308 308 return False
309 309
310 310 def always(self):
311 311 '''Matcher will match everything and .files() will be empty --
312 312 optimization might be possible.'''
313 313 return False
314 314
315 315 def isexact(self):
316 316 '''Matcher will match exactly the list of files in .files() --
317 317 optimization might be possible.'''
318 318 return False
319 319
320 320 def prefix(self):
321 321 '''Matcher will match the paths in .files() recursively --
322 322 optimization might be possible.'''
323 323 return False
324 324
325 325 def anypats(self):
326 326 '''None of .always(), .isexact(), and .prefix() is true --
327 327 optimizations will be difficult.'''
328 328 return not self.always() and not self.isexact() and not self.prefix()
329 329
330 330 class alwaysmatcher(basematcher):
331 331 '''Matches everything.'''
332 332
333 333 def __init__(self, root, cwd, badfn=None, relativeuipath=False):
334 334 super(alwaysmatcher, self).__init__(root, cwd, badfn,
335 335 relativeuipath=relativeuipath)
336 336
337 337 def always(self):
338 338 return True
339 339
340 340 def matchfn(self, f):
341 341 return True
342 342
343 343 def visitdir(self, dir):
344 344 return 'all'
345 345
346 346 def __repr__(self):
347 347 return '<alwaysmatcher>'
348 348
349 349 class nevermatcher(basematcher):
350 350 '''Matches nothing.'''
351 351
352 352 def __init__(self, root, cwd, badfn=None):
353 353 super(nevermatcher, self).__init__(root, cwd, badfn)
354 354
355 355 # It's a little weird to say that the nevermatcher is an exact matcher
356 356 # or a prefix matcher, but it seems to make sense to let callers take
357 357 # fast paths based on either. There will be no exact matches, nor any
358 358 # prefixes (files() returns []), so fast paths iterating over them should
359 359 # be efficient (and correct).
360 360 def isexact(self):
361 361 return True
362 362
363 363 def prefix(self):
364 364 return True
365 365
366 366 def __repr__(self):
367 367 return '<nevermatcher>'
368 368
369 369 class patternmatcher(basematcher):
370 370
371 371 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
372 372 badfn=None):
373 373 super(patternmatcher, self).__init__(root, cwd, badfn)
374 374
375 375 self._files = _explicitfiles(kindpats)
376 376 self._prefix = _prefix(kindpats)
377 377 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '$', listsubrepos,
378 378 root)
379 379
380 380 @propertycache
381 381 def _dirs(self):
382 382 return set(util.dirs(self._fileset)) | {'.'}
383 383
384 384 def visitdir(self, dir):
385 385 if self._prefix and dir in self._fileset:
386 386 return 'all'
387 387 return ('.' in self._fileset or
388 388 dir in self._fileset or
389 389 dir in self._dirs or
390 390 any(parentdir in self._fileset
391 391 for parentdir in util.finddirs(dir)))
392 392
393 393 def prefix(self):
394 394 return self._prefix
395 395
396 396 def __repr__(self):
397 397 return ('<patternmatcher patterns=%r>' % self._pats)
398 398
399 399 class includematcher(basematcher):
400 400
401 401 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
402 402 badfn=None):
403 403 super(includematcher, self).__init__(root, cwd, badfn)
404 404
405 405 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '(?:/|$)',
406 406 listsubrepos, root)
407 407 self._prefix = _prefix(kindpats)
408 408 roots, dirs = _rootsanddirs(kindpats)
409 409 # roots are directories which are recursively included.
410 410 self._roots = set(roots)
411 411 # dirs are directories which are non-recursively included.
412 412 self._dirs = set(dirs)
413 413
414 414 def visitdir(self, dir):
415 415 if self._prefix and dir in self._roots:
416 416 return 'all'
417 417 return ('.' in self._roots or
418 418 dir in self._roots or
419 419 dir in self._dirs or
420 420 any(parentdir in self._roots
421 421 for parentdir in util.finddirs(dir)))
422 422
423 423 def __repr__(self):
424 424 return ('<includematcher includes=%r>' % self._pats)
425 425
426 426 class exactmatcher(basematcher):
427 427 '''Matches the input files exactly. They are interpreted as paths, not
428 428 patterns (so no kind-prefixes).
429 429 '''
430 430
431 431 def __init__(self, root, cwd, files, badfn=None):
432 432 super(exactmatcher, self).__init__(root, cwd, badfn)
433 433
434 434 if isinstance(files, list):
435 435 self._files = files
436 436 else:
437 437 self._files = list(files)
438 438
439 439 matchfn = basematcher.exact
440 440
441 441 @propertycache
442 442 def _dirs(self):
443 443 return set(util.dirs(self._fileset)) | {'.'}
444 444
445 445 def visitdir(self, dir):
446 446 return dir in self._dirs
447 447
448 448 def isexact(self):
449 449 return True
450 450
451 451 def __repr__(self):
452 452 return ('<exactmatcher files=%r>' % self._files)
453 453
454 454 class differencematcher(basematcher):
455 455 '''Composes two matchers by matching if the first matches and the second
456 456 does not. Well, almost... If the user provides a pattern like "-X foo foo",
457 457 Mercurial actually does match "foo" against that. That's because exact
458 458 matches are treated specially. So, since this differencematcher is used for
459 459 excludes, it needs to special-case exact matching.
460 460
461 461 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
462 462 traversedir) are ignored.
463 463
464 464 TODO: If we want to keep the behavior described above for exact matches, we
465 465 should consider instead treating the above case something like this:
466 466 union(exact(foo), difference(pattern(foo), include(foo)))
467 467 '''
468 468 def __init__(self, m1, m2):
469 469 super(differencematcher, self).__init__(m1._root, m1._cwd)
470 470 self._m1 = m1
471 471 self._m2 = m2
472 472 self.bad = m1.bad
473 473 self.explicitdir = m1.explicitdir
474 474 self.traversedir = m1.traversedir
475 475
476 476 def matchfn(self, f):
477 477 return self._m1(f) and (not self._m2(f) or self._m1.exact(f))
478 478
479 479 @propertycache
480 480 def _files(self):
481 481 if self.isexact():
482 482 return [f for f in self._m1.files() if self(f)]
483 483 # If m1 is not an exact matcher, we can't easily figure out the set of
484 484 # files, because its files() are not always files. For example, if
485 485 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
486 486 # want to remove "dir" from the set even though it would match m2,
487 487 # because the "dir" in m1 may not be a file.
488 488 return self._m1.files()
489 489
490 490 def visitdir(self, dir):
491 491 if self._m2.visitdir(dir) == 'all':
492 492 # There's a bug here: If m1 matches file 'dir/file' and m2 excludes
493 493 # 'dir' (recursively), we should still visit 'dir' due to the
494 494 # exception we have for exact matches.
495 495 return False
496 496 return bool(self._m1.visitdir(dir))
497 497
498 498 def isexact(self):
499 499 return self._m1.isexact()
500 500
501 501 def __repr__(self):
502 502 return ('<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2))
503 503
504 504 def intersectmatchers(m1, m2):
505 505 '''Composes two matchers by matching if both of them match.
506 506
507 507 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
508 508 traversedir) are ignored.
509 509 '''
510 510 if m1 is None or m2 is None:
511 511 return m1 or m2
512 512 if m1.always():
513 513 m = copy.copy(m2)
514 514 # TODO: Consider encapsulating these things in a class so there's only
515 515 # one thing to copy from m1.
516 516 m.bad = m1.bad
517 517 m.explicitdir = m1.explicitdir
518 518 m.traversedir = m1.traversedir
519 519 m.abs = m1.abs
520 520 m.rel = m1.rel
521 521 m._relativeuipath |= m1._relativeuipath
522 522 return m
523 523 if m2.always():
524 524 m = copy.copy(m1)
525 525 m._relativeuipath |= m2._relativeuipath
526 526 return m
527 527 return intersectionmatcher(m1, m2)
528 528
529 529 class intersectionmatcher(basematcher):
530 530 def __init__(self, m1, m2):
531 531 super(intersectionmatcher, self).__init__(m1._root, m1._cwd)
532 532 self._m1 = m1
533 533 self._m2 = m2
534 534 self.bad = m1.bad
535 535 self.explicitdir = m1.explicitdir
536 536 self.traversedir = m1.traversedir
537 537
538 538 @propertycache
539 539 def _files(self):
540 540 if self.isexact():
541 541 m1, m2 = self._m1, self._m2
542 542 if not m1.isexact():
543 543 m1, m2 = m2, m1
544 544 return [f for f in m1.files() if m2(f)]
545 545 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
546 546 # the set of files, because their files() are not always files. For
547 547 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
548 548 # "path:dir2", we don't want to remove "dir2" from the set.
549 549 return self._m1.files() + self._m2.files()
550 550
551 551 def matchfn(self, f):
552 552 return self._m1(f) and self._m2(f)
553 553
554 554 def visitdir(self, dir):
555 555 visit1 = self._m1.visitdir(dir)
556 556 if visit1 == 'all':
557 557 return self._m2.visitdir(dir)
558 558 # bool() because visit1=True + visit2='all' should not be 'all'
559 559 return bool(visit1 and self._m2.visitdir(dir))
560 560
561 561 def always(self):
562 562 return self._m1.always() and self._m2.always()
563 563
564 564 def isexact(self):
565 565 return self._m1.isexact() or self._m2.isexact()
566 566
567 567 def __repr__(self):
568 568 return ('<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2))
569 569
570 570 class subdirmatcher(basematcher):
571 571 """Adapt a matcher to work on a subdirectory only.
572 572
573 573 The paths are remapped to remove/insert the path as needed:
574 574
575 575 >>> m1 = match('root', '', ['a.txt', 'sub/b.txt'])
576 576 >>> m2 = subdirmatcher('sub', m1)
577 577 >>> bool(m2('a.txt'))
578 578 False
579 579 >>> bool(m2('b.txt'))
580 580 True
581 581 >>> bool(m2.matchfn('a.txt'))
582 582 False
583 583 >>> bool(m2.matchfn('b.txt'))
584 584 True
585 585 >>> m2.files()
586 586 ['b.txt']
587 587 >>> m2.exact('b.txt')
588 588 True
589 589 >>> util.pconvert(m2.rel('b.txt'))
590 590 'sub/b.txt'
591 591 >>> def bad(f, msg):
592 592 ... print "%s: %s" % (f, msg)
593 593 >>> m1.bad = bad
594 594 >>> m2.bad('x.txt', 'No such file')
595 595 sub/x.txt: No such file
596 596 >>> m2.abs('c.txt')
597 597 'sub/c.txt'
598 598 """
599 599
600 600 def __init__(self, path, matcher):
601 601 super(subdirmatcher, self).__init__(matcher._root, matcher._cwd)
602 602 self._path = path
603 603 self._matcher = matcher
604 604 self._always = matcher.always()
605 605
606 606 self._files = [f[len(path) + 1:] for f in matcher._files
607 607 if f.startswith(path + "/")]
608 608
609 609 # If the parent repo had a path to this subrepo and the matcher is
610 610 # a prefix matcher, this submatcher always matches.
611 611 if matcher.prefix():
612 612 self._always = any(f == path for f in matcher._files)
613 613
614 614 def bad(self, f, msg):
615 615 self._matcher.bad(self._path + "/" + f, msg)
616 616
617 617 def abs(self, f):
618 618 return self._matcher.abs(self._path + "/" + f)
619 619
620 620 def rel(self, f):
621 621 return self._matcher.rel(self._path + "/" + f)
622 622
623 623 def uipath(self, f):
624 624 return self._matcher.uipath(self._path + "/" + f)
625 625
626 626 def matchfn(self, f):
627 627 # Some information is lost in the superclass's constructor, so we
628 628 # can not accurately create the matching function for the subdirectory
629 629 # from the inputs. Instead, we override matchfn() and visitdir() to
630 630 # call the original matcher with the subdirectory path prepended.
631 631 return self._matcher.matchfn(self._path + "/" + f)
632 632
633 633 def visitdir(self, dir):
634 634 if dir == '.':
635 635 dir = self._path
636 636 else:
637 637 dir = self._path + "/" + dir
638 638 return self._matcher.visitdir(dir)
639 639
640 640 def always(self):
641 641 return self._always
642 642
643 643 def prefix(self):
644 644 return self._matcher.prefix() and not self._always
645 645
646 646 def __repr__(self):
647 647 return ('<subdirmatcher path=%r, matcher=%r>' %
648 648 (self._path, self._matcher))
649 649
650 class forceincludematcher(basematcher):
651 """A matcher that returns true for any of the forced includes before testing
652 against the actual matcher."""
653 def __init__(self, matcher, includes):
654 self._matcher = matcher
655 self._includes = includes
656
657 def matchfn(self, f):
658 return f in self._includes or self._matcher(f)
659
660 def __repr__(self):
661 return ('<forceincludematcher matcher=%r, includes=%r>' %
662 (self._matcher, sorted(self._includes)))
663
664 650 class unionmatcher(basematcher):
665 651 """A matcher that is the union of several matchers."""
666 652 def __init__(self, matchers):
667 653 self._matchers = matchers
668 654
669 655 def matchfn(self, f):
670 656 for match in self._matchers:
671 657 if match(f):
672 658 return True
673 659 return False
674 660
675 661 def __repr__(self):
676 662 return ('<unionmatcher matchers=%r>' % self._matchers)
677 663
678 664 class negatematcher(basematcher):
679 665 def __init__(self, matcher):
680 666 self._matcher = matcher
681 667
682 668 def matchfn(self, f):
683 669 return not self._matcher(f)
684 670
685 671 def __repr__(self):
686 672 return ('<negatematcher matcher=%r>' % self._matcher)
687 673
688 674 def patkind(pattern, default=None):
689 675 '''If pattern is 'kind:pat' with a known kind, return kind.'''
690 676 return _patsplit(pattern, default)[0]
691 677
692 678 def _patsplit(pattern, default):
693 679 """Split a string into the optional pattern kind prefix and the actual
694 680 pattern."""
695 681 if ':' in pattern:
696 682 kind, pat = pattern.split(':', 1)
697 683 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
698 684 'listfile', 'listfile0', 'set', 'include', 'subinclude',
699 685 'rootfilesin'):
700 686 return kind, pat
701 687 return default, pattern
702 688
703 689 def _globre(pat):
704 690 r'''Convert an extended glob string to a regexp string.
705 691
706 692 >>> print _globre(r'?')
707 693 .
708 694 >>> print _globre(r'*')
709 695 [^/]*
710 696 >>> print _globre(r'**')
711 697 .*
712 698 >>> print _globre(r'**/a')
713 699 (?:.*/)?a
714 700 >>> print _globre(r'a/**/b')
715 701 a\/(?:.*/)?b
716 702 >>> print _globre(r'[a*?!^][^b][!c]')
717 703 [a*?!^][\^b][^c]
718 704 >>> print _globre(r'{a,b}')
719 705 (?:a|b)
720 706 >>> print _globre(r'.\*\?')
721 707 \.\*\?
722 708 '''
723 709 i, n = 0, len(pat)
724 710 res = ''
725 711 group = 0
726 712 escape = util.re.escape
727 713 def peek():
728 714 return i < n and pat[i:i + 1]
729 715 while i < n:
730 716 c = pat[i:i + 1]
731 717 i += 1
732 718 if c not in '*?[{},\\':
733 719 res += escape(c)
734 720 elif c == '*':
735 721 if peek() == '*':
736 722 i += 1
737 723 if peek() == '/':
738 724 i += 1
739 725 res += '(?:.*/)?'
740 726 else:
741 727 res += '.*'
742 728 else:
743 729 res += '[^/]*'
744 730 elif c == '?':
745 731 res += '.'
746 732 elif c == '[':
747 733 j = i
748 734 if j < n and pat[j:j + 1] in '!]':
749 735 j += 1
750 736 while j < n and pat[j:j + 1] != ']':
751 737 j += 1
752 738 if j >= n:
753 739 res += '\\['
754 740 else:
755 741 stuff = pat[i:j].replace('\\','\\\\')
756 742 i = j + 1
757 743 if stuff[0:1] == '!':
758 744 stuff = '^' + stuff[1:]
759 745 elif stuff[0:1] == '^':
760 746 stuff = '\\' + stuff
761 747 res = '%s[%s]' % (res, stuff)
762 748 elif c == '{':
763 749 group += 1
764 750 res += '(?:'
765 751 elif c == '}' and group:
766 752 res += ')'
767 753 group -= 1
768 754 elif c == ',' and group:
769 755 res += '|'
770 756 elif c == '\\':
771 757 p = peek()
772 758 if p:
773 759 i += 1
774 760 res += escape(p)
775 761 else:
776 762 res += escape(c)
777 763 else:
778 764 res += escape(c)
779 765 return res
780 766
781 767 def _regex(kind, pat, globsuffix):
782 768 '''Convert a (normalized) pattern of any kind into a regular expression.
783 769 globsuffix is appended to the regexp of globs.'''
784 770 if not pat:
785 771 return ''
786 772 if kind == 're':
787 773 return pat
788 774 if kind in ('path', 'relpath'):
789 775 if pat == '.':
790 776 return ''
791 777 return util.re.escape(pat) + '(?:/|$)'
792 778 if kind == 'rootfilesin':
793 779 if pat == '.':
794 780 escaped = ''
795 781 else:
796 782 # Pattern is a directory name.
797 783 escaped = util.re.escape(pat) + '/'
798 784 # Anything after the pattern must be a non-directory.
799 785 return escaped + '[^/]+$'
800 786 if kind == 'relglob':
801 787 return '(?:|.*/)' + _globre(pat) + globsuffix
802 788 if kind == 'relre':
803 789 if pat.startswith('^'):
804 790 return pat
805 791 return '.*' + pat
806 792 return _globre(pat) + globsuffix
807 793
808 794 def _buildmatch(ctx, kindpats, globsuffix, listsubrepos, root):
809 795 '''Return regexp string and a matcher function for kindpats.
810 796 globsuffix is appended to the regexp of globs.'''
811 797 matchfuncs = []
812 798
813 799 subincludes, kindpats = _expandsubinclude(kindpats, root)
814 800 if subincludes:
815 801 submatchers = {}
816 802 def matchsubinclude(f):
817 803 for prefix, matcherargs in subincludes:
818 804 if f.startswith(prefix):
819 805 mf = submatchers.get(prefix)
820 806 if mf is None:
821 807 mf = match(*matcherargs)
822 808 submatchers[prefix] = mf
823 809
824 810 if mf(f[len(prefix):]):
825 811 return True
826 812 return False
827 813 matchfuncs.append(matchsubinclude)
828 814
829 815 fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
830 816 if fset:
831 817 matchfuncs.append(fset.__contains__)
832 818
833 819 regex = ''
834 820 if kindpats:
835 821 regex, mf = _buildregexmatch(kindpats, globsuffix)
836 822 matchfuncs.append(mf)
837 823
838 824 if len(matchfuncs) == 1:
839 825 return regex, matchfuncs[0]
840 826 else:
841 827 return regex, lambda f: any(mf(f) for mf in matchfuncs)
842 828
843 829 def _buildregexmatch(kindpats, globsuffix):
844 830 """Build a match function from a list of kinds and kindpats,
845 831 return regexp string and a matcher function."""
846 832 try:
847 833 regex = '(?:%s)' % '|'.join([_regex(k, p, globsuffix)
848 834 for (k, p, s) in kindpats])
849 835 if len(regex) > 20000:
850 836 raise OverflowError
851 837 return regex, _rematcher(regex)
852 838 except OverflowError:
853 839 # We're using a Python with a tiny regex engine and we
854 840 # made it explode, so we'll divide the pattern list in two
855 841 # until it works
856 842 l = len(kindpats)
857 843 if l < 2:
858 844 raise
859 845 regexa, a = _buildregexmatch(kindpats[:l//2], globsuffix)
860 846 regexb, b = _buildregexmatch(kindpats[l//2:], globsuffix)
861 847 return regex, lambda s: a(s) or b(s)
862 848 except re.error:
863 849 for k, p, s in kindpats:
864 850 try:
865 851 _rematcher('(?:%s)' % _regex(k, p, globsuffix))
866 852 except re.error:
867 853 if s:
868 854 raise error.Abort(_("%s: invalid pattern (%s): %s") %
869 855 (s, k, p))
870 856 else:
871 857 raise error.Abort(_("invalid pattern (%s): %s") % (k, p))
872 858 raise error.Abort(_("invalid pattern"))
873 859
874 860 def _patternrootsanddirs(kindpats):
875 861 '''Returns roots and directories corresponding to each pattern.
876 862
877 863 This calculates the roots and directories exactly matching the patterns and
878 864 returns a tuple of (roots, dirs) for each. It does not return other
879 865 directories which may also need to be considered, like the parent
880 866 directories.
881 867 '''
882 868 r = []
883 869 d = []
884 870 for kind, pat, source in kindpats:
885 871 if kind == 'glob': # find the non-glob prefix
886 872 root = []
887 873 for p in pat.split('/'):
888 874 if '[' in p or '{' in p or '*' in p or '?' in p:
889 875 break
890 876 root.append(p)
891 877 r.append('/'.join(root) or '.')
892 878 elif kind in ('relpath', 'path'):
893 879 r.append(pat or '.')
894 880 elif kind in ('rootfilesin',):
895 881 d.append(pat or '.')
896 882 else: # relglob, re, relre
897 883 r.append('.')
898 884 return r, d
899 885
900 886 def _roots(kindpats):
901 887 '''Returns root directories to match recursively from the given patterns.'''
902 888 roots, dirs = _patternrootsanddirs(kindpats)
903 889 return roots
904 890
905 891 def _rootsanddirs(kindpats):
906 892 '''Returns roots and exact directories from patterns.
907 893
908 894 roots are directories to match recursively, whereas exact directories should
909 895 be matched non-recursively. The returned (roots, dirs) tuple will also
910 896 include directories that need to be implicitly considered as either, such as
911 897 parent directories.
912 898
913 899 >>> _rootsanddirs(\
914 900 [('glob', 'g/h/*', ''), ('glob', 'g/h', ''), ('glob', 'g*', '')])
915 901 (['g/h', 'g/h', '.'], ['g', '.'])
916 902 >>> _rootsanddirs(\
917 903 [('rootfilesin', 'g/h', ''), ('rootfilesin', '', '')])
918 904 ([], ['g/h', '.', 'g', '.'])
919 905 >>> _rootsanddirs(\
920 906 [('relpath', 'r', ''), ('path', 'p/p', ''), ('path', '', '')])
921 907 (['r', 'p/p', '.'], ['p', '.'])
922 908 >>> _rootsanddirs(\
923 909 [('relglob', 'rg*', ''), ('re', 're/', ''), ('relre', 'rr', '')])
924 910 (['.', '.', '.'], ['.'])
925 911 '''
926 912 r, d = _patternrootsanddirs(kindpats)
927 913
928 914 # Append the parents as non-recursive/exact directories, since they must be
929 915 # scanned to get to either the roots or the other exact directories.
930 916 d.extend(util.dirs(d))
931 917 d.extend(util.dirs(r))
932 918 # util.dirs() does not include the root directory, so add it manually
933 919 d.append('.')
934 920
935 921 return r, d
936 922
937 923 def _explicitfiles(kindpats):
938 924 '''Returns the potential explicit filenames from the patterns.
939 925
940 926 >>> _explicitfiles([('path', 'foo/bar', '')])
941 927 ['foo/bar']
942 928 >>> _explicitfiles([('rootfilesin', 'foo/bar', '')])
943 929 []
944 930 '''
945 931 # Keep only the pattern kinds where one can specify filenames (vs only
946 932 # directory names).
947 933 filable = [kp for kp in kindpats if kp[0] not in ('rootfilesin',)]
948 934 return _roots(filable)
949 935
950 936 def _prefix(kindpats):
951 937 '''Whether all the patterns match a prefix (i.e. recursively)'''
952 938 for kind, pat, source in kindpats:
953 939 if kind not in ('path', 'relpath'):
954 940 return False
955 941 return True
956 942
957 943 _commentre = None
958 944
959 945 def readpatternfile(filepath, warn, sourceinfo=False):
960 946 '''parse a pattern file, returning a list of
961 947 patterns. These patterns should be given to compile()
962 948 to be validated and converted into a match function.
963 949
964 950 trailing white space is dropped.
965 951 the escape character is backslash.
966 952 comments start with #.
967 953 empty lines are skipped.
968 954
969 955 lines can be of the following formats:
970 956
971 957 syntax: regexp # defaults following lines to non-rooted regexps
972 958 syntax: glob # defaults following lines to non-rooted globs
973 959 re:pattern # non-rooted regular expression
974 960 glob:pattern # non-rooted glob
975 961 pattern # pattern of the current default type
976 962
977 963 if sourceinfo is set, returns a list of tuples:
978 964 (pattern, lineno, originalline). This is useful to debug ignore patterns.
979 965 '''
980 966
981 967 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
982 968 'include': 'include', 'subinclude': 'subinclude'}
983 969 syntax = 'relre:'
984 970 patterns = []
985 971
986 972 fp = open(filepath, 'rb')
987 973 for lineno, line in enumerate(util.iterfile(fp), start=1):
988 974 if "#" in line:
989 975 global _commentre
990 976 if not _commentre:
991 977 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
992 978 # remove comments prefixed by an even number of escapes
993 979 m = _commentre.search(line)
994 980 if m:
995 981 line = line[:m.end(1)]
996 982 # fixup properly escaped comments that survived the above
997 983 line = line.replace("\\#", "#")
998 984 line = line.rstrip()
999 985 if not line:
1000 986 continue
1001 987
1002 988 if line.startswith('syntax:'):
1003 989 s = line[7:].strip()
1004 990 try:
1005 991 syntax = syntaxes[s]
1006 992 except KeyError:
1007 993 if warn:
1008 994 warn(_("%s: ignoring invalid syntax '%s'\n") %
1009 995 (filepath, s))
1010 996 continue
1011 997
1012 998 linesyntax = syntax
1013 999 for s, rels in syntaxes.iteritems():
1014 1000 if line.startswith(rels):
1015 1001 linesyntax = rels
1016 1002 line = line[len(rels):]
1017 1003 break
1018 1004 elif line.startswith(s+':'):
1019 1005 linesyntax = rels
1020 1006 line = line[len(s) + 1:]
1021 1007 break
1022 1008 if sourceinfo:
1023 1009 patterns.append((linesyntax + line, lineno, line))
1024 1010 else:
1025 1011 patterns.append(linesyntax + line)
1026 1012 fp.close()
1027 1013 return patterns
@@ -1,671 +1,678
1 1 # sparse.py - functionality for sparse checkouts
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
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 hashlib
12 12 import os
13 13
14 14 from .i18n import _
15 15 from .node import nullid
16 16 from . import (
17 17 error,
18 18 match as matchmod,
19 19 merge as mergemod,
20 20 pycompat,
21 21 util,
22 22 )
23 23
24 24 # Whether sparse features are enabled. This variable is intended to be
25 25 # temporary to facilitate porting sparse to core. It should eventually be
26 26 # a per-repo option, possibly a repo requirement.
27 27 enabled = False
28 28
29 29 def parseconfig(ui, raw):
30 30 """Parse sparse config file content.
31 31
32 32 Returns a tuple of includes, excludes, and profiles.
33 33 """
34 34 includes = set()
35 35 excludes = set()
36 36 current = includes
37 37 profiles = []
38 38 for line in raw.split('\n'):
39 39 line = line.strip()
40 40 if not line or line.startswith('#'):
41 41 # empty or comment line, skip
42 42 continue
43 43 elif line.startswith('%include '):
44 44 line = line[9:].strip()
45 45 if line:
46 46 profiles.append(line)
47 47 elif line == '[include]':
48 48 if current != includes:
49 49 # TODO pass filename into this API so we can report it.
50 50 raise error.Abort(_('sparse config cannot have includes ' +
51 51 'after excludes'))
52 52 continue
53 53 elif line == '[exclude]':
54 54 current = excludes
55 55 elif line:
56 56 if line.strip().startswith('/'):
57 57 ui.warn(_('warning: sparse profile cannot use' +
58 58 ' paths starting with /, ignoring %s\n') % line)
59 59 continue
60 60 current.add(line)
61 61
62 62 return includes, excludes, profiles
63 63
64 64 # Exists as separate function to facilitate monkeypatching.
65 65 def readprofile(repo, profile, changeid):
66 66 """Resolve the raw content of a sparse profile file."""
67 67 # TODO add some kind of cache here because this incurs a manifest
68 68 # resolve and can be slow.
69 69 return repo.filectx(profile, changeid=changeid).data()
70 70
71 71 def patternsforrev(repo, rev):
72 72 """Obtain sparse checkout patterns for the given rev.
73 73
74 74 Returns a tuple of iterables representing includes, excludes, and
75 75 patterns.
76 76 """
77 77 # Feature isn't enabled. No-op.
78 78 if not enabled:
79 79 return set(), set(), []
80 80
81 81 raw = repo.vfs.tryread('sparse')
82 82 if not raw:
83 83 return set(), set(), []
84 84
85 85 if rev is None:
86 86 raise error.Abort(_('cannot parse sparse patterns from working '
87 87 'directory'))
88 88
89 89 includes, excludes, profiles = parseconfig(repo.ui, raw)
90 90 ctx = repo[rev]
91 91
92 92 if profiles:
93 93 visited = set()
94 94 while profiles:
95 95 profile = profiles.pop()
96 96 if profile in visited:
97 97 continue
98 98
99 99 visited.add(profile)
100 100
101 101 try:
102 102 raw = readprofile(repo, profile, rev)
103 103 except error.ManifestLookupError:
104 104 msg = (
105 105 "warning: sparse profile '%s' not found "
106 106 "in rev %s - ignoring it\n" % (profile, ctx))
107 107 # experimental config: sparse.missingwarning
108 108 if repo.ui.configbool(
109 109 'sparse', 'missingwarning', True):
110 110 repo.ui.warn(msg)
111 111 else:
112 112 repo.ui.debug(msg)
113 113 continue
114 114
115 115 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
116 116 includes.update(pincludes)
117 117 excludes.update(pexcludes)
118 118 for subprofile in subprofs:
119 119 profiles.append(subprofile)
120 120
121 121 profiles = visited
122 122
123 123 if includes:
124 124 includes.add('.hg*')
125 125
126 126 return includes, excludes, profiles
127 127
128 128 def activeconfig(repo):
129 129 """Determine the active sparse config rules.
130 130
131 131 Rules are constructed by reading the current sparse config and bringing in
132 132 referenced profiles from parents of the working directory.
133 133 """
134 134 revs = [repo.changelog.rev(node) for node in
135 135 repo.dirstate.parents() if node != nullid]
136 136
137 137 allincludes = set()
138 138 allexcludes = set()
139 139 allprofiles = set()
140 140
141 141 for rev in revs:
142 142 includes, excludes, profiles = patternsforrev(repo, rev)
143 143 allincludes |= includes
144 144 allexcludes |= excludes
145 145 allprofiles |= set(profiles)
146 146
147 147 return allincludes, allexcludes, allprofiles
148 148
149 149 def configsignature(repo, includetemp=True):
150 150 """Obtain the signature string for the current sparse configuration.
151 151
152 152 This is used to construct a cache key for matchers.
153 153 """
154 154 cache = repo._sparsesignaturecache
155 155
156 156 signature = cache.get('signature')
157 157
158 158 if includetemp:
159 159 tempsignature = cache.get('tempsignature')
160 160 else:
161 161 tempsignature = '0'
162 162
163 163 if signature is None or (includetemp and tempsignature is None):
164 164 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
165 165 cache['signature'] = signature
166 166
167 167 if includetemp:
168 168 raw = repo.vfs.tryread('tempsparse')
169 169 tempsignature = hashlib.sha1(raw).hexdigest()
170 170 cache['tempsignature'] = tempsignature
171 171
172 172 return '%s %s' % (signature, tempsignature)
173 173
174 174 def writeconfig(repo, includes, excludes, profiles):
175 175 """Write the sparse config file given a sparse configuration."""
176 176 with repo.vfs('sparse', 'wb') as fh:
177 177 for p in sorted(profiles):
178 178 fh.write('%%include %s\n' % p)
179 179
180 180 if includes:
181 181 fh.write('[include]\n')
182 182 for i in sorted(includes):
183 183 fh.write(i)
184 184 fh.write('\n')
185 185
186 186 if excludes:
187 187 fh.write('[exclude]\n')
188 188 for e in sorted(excludes):
189 189 fh.write(e)
190 190 fh.write('\n')
191 191
192 192 repo._sparsesignaturecache.clear()
193 193
194 194 def readtemporaryincludes(repo):
195 195 raw = repo.vfs.tryread('tempsparse')
196 196 if not raw:
197 197 return set()
198 198
199 199 return set(raw.split('\n'))
200 200
201 201 def writetemporaryincludes(repo, includes):
202 202 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
203 203 repo._sparsesignaturecache.clear()
204 204
205 205 def addtemporaryincludes(repo, additional):
206 206 includes = readtemporaryincludes(repo)
207 207 for i in additional:
208 208 includes.add(i)
209 209 writetemporaryincludes(repo, includes)
210 210
211 211 def prunetemporaryincludes(repo):
212 212 if not enabled or not repo.vfs.exists('tempsparse'):
213 213 return
214 214
215 215 s = repo.status()
216 216 if s.modified or s.added or s.removed or s.deleted:
217 217 # Still have pending changes. Don't bother trying to prune.
218 218 return
219 219
220 220 sparsematch = matcher(repo, includetemp=False)
221 221 dirstate = repo.dirstate
222 222 actions = []
223 223 dropped = []
224 224 tempincludes = readtemporaryincludes(repo)
225 225 for file in tempincludes:
226 226 if file in dirstate and not sparsematch(file):
227 227 message = _('dropping temporarily included sparse files')
228 228 actions.append((file, None, message))
229 229 dropped.append(file)
230 230
231 231 typeactions = collections.defaultdict(list)
232 232 typeactions['r'] = actions
233 233 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
234 234
235 235 # Fix dirstate
236 236 for file in dropped:
237 237 dirstate.drop(file)
238 238
239 239 repo.vfs.unlink('tempsparse')
240 240 repo._sparsesignaturecache.clear()
241 241 msg = _('cleaned up %d temporarily added file(s) from the '
242 242 'sparse checkout\n')
243 243 repo.ui.status(msg % len(tempincludes))
244 244
245 def forceincludematcher(matcher, includes):
246 """Returns a matcher that returns true for any of the forced includes
247 before testing against the actual matcher."""
248 kindpats = [('path', include, '') for include in includes]
249 includematcher = matchmod.includematcher('', '', kindpats)
250 return matchmod.unionmatcher([includematcher, matcher])
251
245 252 def matcher(repo, revs=None, includetemp=True):
246 253 """Obtain a matcher for sparse working directories for the given revs.
247 254
248 255 If multiple revisions are specified, the matcher is the union of all
249 256 revs.
250 257
251 258 ``includetemp`` indicates whether to use the temporary sparse profile.
252 259 """
253 260 # If sparse isn't enabled, sparse matcher matches everything.
254 261 if not enabled:
255 262 return matchmod.always(repo.root, '')
256 263
257 264 if not revs or revs == [None]:
258 265 revs = [repo.changelog.rev(node)
259 266 for node in repo.dirstate.parents() if node != nullid]
260 267
261 268 signature = configsignature(repo, includetemp=includetemp)
262 269
263 270 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
264 271
265 272 result = repo._sparsematchercache.get(key)
266 273 if result:
267 274 return result
268 275
269 276 matchers = []
270 277 for rev in revs:
271 278 try:
272 279 includes, excludes, profiles = patternsforrev(repo, rev)
273 280
274 281 if includes or excludes:
275 282 # Explicitly include subdirectories of includes so
276 283 # status will walk them down to the actual include.
277 284 subdirs = set()
278 285 for include in includes:
279 286 # TODO consider using posix path functions here so Windows
280 287 # \ directory separators don't come into play.
281 288 dirname = os.path.dirname(include)
282 289 # basename is used to avoid issues with absolute
283 290 # paths (which on Windows can include the drive).
284 291 while os.path.basename(dirname):
285 292 subdirs.add(dirname)
286 293 dirname = os.path.dirname(dirname)
287 294
288 295 matcher = matchmod.match(repo.root, '', [],
289 296 include=includes, exclude=excludes,
290 297 default='relpath')
291 298 if subdirs:
292 matcher = matchmod.forceincludematcher(matcher, subdirs)
299 matcher = forceincludematcher(matcher, subdirs)
293 300 matchers.append(matcher)
294 301 except IOError:
295 302 pass
296 303
297 304 if not matchers:
298 305 result = matchmod.always(repo.root, '')
299 306 elif len(matchers) == 1:
300 307 result = matchers[0]
301 308 else:
302 309 result = matchmod.unionmatcher(matchers)
303 310
304 311 if includetemp:
305 312 tempincludes = readtemporaryincludes(repo)
306 result = matchmod.forceincludematcher(result, tempincludes)
313 result = forceincludematcher(result, tempincludes)
307 314
308 315 repo._sparsematchercache[key] = result
309 316
310 317 return result
311 318
312 319 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
313 320 """Filter updates to only lay out files that match the sparse rules."""
314 321 if not enabled:
315 322 return actions
316 323
317 324 oldrevs = [pctx.rev() for pctx in wctx.parents()]
318 325 oldsparsematch = matcher(repo, oldrevs)
319 326
320 327 if oldsparsematch.always():
321 328 return actions
322 329
323 330 files = set()
324 331 prunedactions = {}
325 332
326 333 if branchmerge:
327 334 # If we're merging, use the wctx filter, since we're merging into
328 335 # the wctx.
329 336 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
330 337 else:
331 338 # If we're updating, use the target context's filter, since we're
332 339 # moving to the target context.
333 340 sparsematch = matcher(repo, [mctx.rev()])
334 341
335 342 temporaryfiles = []
336 343 for file, action in actions.iteritems():
337 344 type, args, msg = action
338 345 files.add(file)
339 346 if sparsematch(file):
340 347 prunedactions[file] = action
341 348 elif type == 'm':
342 349 temporaryfiles.append(file)
343 350 prunedactions[file] = action
344 351 elif branchmerge:
345 352 if type != 'k':
346 353 temporaryfiles.append(file)
347 354 prunedactions[file] = action
348 355 elif type == 'f':
349 356 prunedactions[file] = action
350 357 elif file in wctx:
351 358 prunedactions[file] = ('r', args, msg)
352 359
353 360 if len(temporaryfiles) > 0:
354 361 repo.ui.status(_('temporarily included %d file(s) in the sparse '
355 362 'checkout for merging\n') % len(temporaryfiles))
356 363 addtemporaryincludes(repo, temporaryfiles)
357 364
358 365 # Add the new files to the working copy so they can be merged, etc
359 366 actions = []
360 367 message = 'temporarily adding to sparse checkout'
361 368 wctxmanifest = repo[None].manifest()
362 369 for file in temporaryfiles:
363 370 if file in wctxmanifest:
364 371 fctx = repo[None][file]
365 372 actions.append((file, (fctx.flags(), False), message))
366 373
367 374 typeactions = collections.defaultdict(list)
368 375 typeactions['g'] = actions
369 376 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
370 377 False)
371 378
372 379 dirstate = repo.dirstate
373 380 for file, flags, msg in actions:
374 381 dirstate.normal(file)
375 382
376 383 profiles = activeconfig(repo)[2]
377 384 changedprofiles = profiles & files
378 385 # If an active profile changed during the update, refresh the checkout.
379 386 # Don't do this during a branch merge, since all incoming changes should
380 387 # have been handled by the temporary includes above.
381 388 if changedprofiles and not branchmerge:
382 389 mf = mctx.manifest()
383 390 for file in mf:
384 391 old = oldsparsematch(file)
385 392 new = sparsematch(file)
386 393 if not old and new:
387 394 flags = mf.flags(file)
388 395 prunedactions[file] = ('g', (flags, False), '')
389 396 elif old and not new:
390 397 prunedactions[file] = ('r', [], '')
391 398
392 399 return prunedactions
393 400
394 401 def refreshwdir(repo, origstatus, origsparsematch, force=False):
395 402 """Refreshes working directory by taking sparse config into account.
396 403
397 404 The old status and sparse matcher is compared against the current sparse
398 405 matcher.
399 406
400 407 Will abort if a file with pending changes is being excluded or included
401 408 unless ``force`` is True.
402 409 """
403 410 # Verify there are no pending changes
404 411 pending = set()
405 412 pending.update(origstatus.modified)
406 413 pending.update(origstatus.added)
407 414 pending.update(origstatus.removed)
408 415 sparsematch = matcher(repo)
409 416 abort = False
410 417
411 418 for f in pending:
412 419 if not sparsematch(f):
413 420 repo.ui.warn(_("pending changes to '%s'\n") % f)
414 421 abort = not force
415 422
416 423 if abort:
417 424 raise error.Abort(_('could not update sparseness due to pending '
418 425 'changes'))
419 426
420 427 # Calculate actions
421 428 dirstate = repo.dirstate
422 429 ctx = repo['.']
423 430 added = []
424 431 lookup = []
425 432 dropped = []
426 433 mf = ctx.manifest()
427 434 files = set(mf)
428 435
429 436 actions = {}
430 437
431 438 for file in files:
432 439 old = origsparsematch(file)
433 440 new = sparsematch(file)
434 441 # Add files that are newly included, or that don't exist in
435 442 # the dirstate yet.
436 443 if (new and not old) or (old and new and not file in dirstate):
437 444 fl = mf.flags(file)
438 445 if repo.wvfs.exists(file):
439 446 actions[file] = ('e', (fl,), '')
440 447 lookup.append(file)
441 448 else:
442 449 actions[file] = ('g', (fl, False), '')
443 450 added.append(file)
444 451 # Drop files that are newly excluded, or that still exist in
445 452 # the dirstate.
446 453 elif (old and not new) or (not old and not new and file in dirstate):
447 454 dropped.append(file)
448 455 if file not in pending:
449 456 actions[file] = ('r', [], '')
450 457
451 458 # Verify there are no pending changes in newly included files
452 459 abort = False
453 460 for file in lookup:
454 461 repo.ui.warn(_("pending changes to '%s'\n") % file)
455 462 abort = not force
456 463 if abort:
457 464 raise error.Abort(_('cannot change sparseness due to pending '
458 465 'changes (delete the files or use '
459 466 '--force to bring them back dirty)'))
460 467
461 468 # Check for files that were only in the dirstate.
462 469 for file, state in dirstate.iteritems():
463 470 if not file in files:
464 471 old = origsparsematch(file)
465 472 new = sparsematch(file)
466 473 if old and not new:
467 474 dropped.append(file)
468 475
469 476 # Apply changes to disk
470 477 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
471 478 for f, (m, args, msg) in actions.iteritems():
472 479 if m not in typeactions:
473 480 typeactions[m] = []
474 481 typeactions[m].append((f, args, msg))
475 482
476 483 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
477 484
478 485 # Fix dirstate
479 486 for file in added:
480 487 dirstate.normal(file)
481 488
482 489 for file in dropped:
483 490 dirstate.drop(file)
484 491
485 492 for file in lookup:
486 493 # File exists on disk, and we're bringing it back in an unknown state.
487 494 dirstate.normallookup(file)
488 495
489 496 return added, dropped, lookup
490 497
491 498 def aftercommit(repo, node):
492 499 """Perform actions after a working directory commit."""
493 500 # This function is called unconditionally, even if sparse isn't
494 501 # enabled.
495 502 ctx = repo[node]
496 503
497 504 profiles = patternsforrev(repo, ctx.rev())[2]
498 505
499 506 # profiles will only have data if sparse is enabled.
500 507 if set(profiles) & set(ctx.files()):
501 508 origstatus = repo.status()
502 509 origsparsematch = matcher(repo)
503 510 refreshwdir(repo, origstatus, origsparsematch, force=True)
504 511
505 512 prunetemporaryincludes(repo)
506 513
507 514 def clearrules(repo, force=False):
508 515 """Clears include/exclude rules from the sparse config.
509 516
510 517 The remaining sparse config only has profiles, if defined. The working
511 518 directory is refreshed, as needed.
512 519 """
513 520 with repo.wlock():
514 521 raw = repo.vfs.tryread('sparse')
515 522 includes, excludes, profiles = parseconfig(repo.ui, raw)
516 523
517 524 if not includes and not excludes:
518 525 return
519 526
520 527 oldstatus = repo.status()
521 528 oldmatch = matcher(repo)
522 529 writeconfig(repo, set(), set(), profiles)
523 530 refreshwdir(repo, oldstatus, oldmatch, force=force)
524 531
525 532 def importfromfiles(repo, opts, paths, force=False):
526 533 """Import sparse config rules from files.
527 534
528 535 The updated sparse config is written out and the working directory
529 536 is refreshed, as needed.
530 537 """
531 538 with repo.wlock():
532 539 # read current configuration
533 540 raw = repo.vfs.tryread('sparse')
534 541 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
535 542 includes, excludes, profiles = map(
536 543 set, (oincludes, oexcludes, oprofiles))
537 544
538 545 aincludes, aexcludes, aprofiles = activeconfig(repo)
539 546
540 547 # Import rules on top; only take in rules that are not yet
541 548 # part of the active rules.
542 549 changed = False
543 550 for p in paths:
544 551 with util.posixfile(util.expandpath(p)) as fh:
545 552 raw = fh.read()
546 553
547 554 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
548 555 oldsize = len(includes) + len(excludes) + len(profiles)
549 556 includes.update(iincludes - aincludes)
550 557 excludes.update(iexcludes - aexcludes)
551 558 profiles.update(set(iprofiles) - aprofiles)
552 559 if len(includes) + len(excludes) + len(profiles) > oldsize:
553 560 changed = True
554 561
555 562 profilecount = includecount = excludecount = 0
556 563 fcounts = (0, 0, 0)
557 564
558 565 if changed:
559 566 profilecount = len(profiles - aprofiles)
560 567 includecount = len(includes - aincludes)
561 568 excludecount = len(excludes - aexcludes)
562 569
563 570 oldstatus = repo.status()
564 571 oldsparsematch = matcher(repo)
565 572
566 573 # TODO remove this try..except once the matcher integrates better
567 574 # with dirstate. We currently have to write the updated config
568 575 # because that will invalidate the matcher cache and force a
569 576 # re-read. We ideally want to update the cached matcher on the
570 577 # repo instance then flush the new config to disk once wdir is
571 578 # updated. But this requires massive rework to matcher() and its
572 579 # consumers.
573 580 writeconfig(repo, includes, excludes, profiles)
574 581
575 582 try:
576 583 fcounts = map(
577 584 len,
578 585 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
579 586 except Exception:
580 587 writeconfig(repo, oincludes, oexcludes, oprofiles)
581 588 raise
582 589
583 590 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
584 591 *fcounts)
585 592
586 593 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
587 594 delete=False, enableprofile=False, disableprofile=False,
588 595 force=False):
589 596 """Perform a sparse config update.
590 597
591 598 Only one of the actions may be performed.
592 599
593 600 The new config is written out and a working directory refresh is performed.
594 601 """
595 602 with repo.wlock():
596 603 oldmatcher = matcher(repo)
597 604
598 605 raw = repo.vfs.tryread('sparse')
599 606 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
600 607 oldprofiles = set(oldprofiles)
601 608
602 609 if reset:
603 610 newinclude = set()
604 611 newexclude = set()
605 612 newprofiles = set()
606 613 else:
607 614 newinclude = set(oldinclude)
608 615 newexclude = set(oldexclude)
609 616 newprofiles = set(oldprofiles)
610 617
611 618 oldstatus = repo.status()
612 619
613 620 if any(pat.startswith('/') for pat in pats):
614 621 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
615 622 % ([pat for pat in pats if pat.startswith('/')]))
616 623 elif include:
617 624 newinclude.update(pats)
618 625 elif exclude:
619 626 newexclude.update(pats)
620 627 elif enableprofile:
621 628 newprofiles.update(pats)
622 629 elif disableprofile:
623 630 newprofiles.difference_update(pats)
624 631 elif delete:
625 632 newinclude.difference_update(pats)
626 633 newexclude.difference_update(pats)
627 634
628 635 profilecount = (len(newprofiles - oldprofiles) -
629 636 len(oldprofiles - newprofiles))
630 637 includecount = (len(newinclude - oldinclude) -
631 638 len(oldinclude - newinclude))
632 639 excludecount = (len(newexclude - oldexclude) -
633 640 len(oldexclude - newexclude))
634 641
635 642 # TODO clean up this writeconfig() + try..except pattern once we can.
636 643 # See comment in importfromfiles() explaining it.
637 644 writeconfig(repo, newinclude, newexclude, newprofiles)
638 645
639 646 try:
640 647 fcounts = map(
641 648 len,
642 649 refreshwdir(repo, oldstatus, oldmatcher, force=force))
643 650
644 651 printchanges(repo.ui, opts, profilecount, includecount,
645 652 excludecount, *fcounts)
646 653 except Exception:
647 654 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
648 655 raise
649 656
650 657 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
651 658 added=0, dropped=0, conflicting=0):
652 659 """Print output summarizing sparse config changes."""
653 660 with ui.formatter('sparse', opts) as fm:
654 661 fm.startitem()
655 662 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
656 663 profilecount)
657 664 fm.condwrite(ui.verbose, 'include_rules_added',
658 665 _('Include rules changed: %d\n'), includecount)
659 666 fm.condwrite(ui.verbose, 'exclude_rules_added',
660 667 _('Exclude rules changed: %d\n'), excludecount)
661 668
662 669 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
663 670 # files are added or removed outside of the templating formatter
664 671 # framework. No point in repeating ourselves in that case.
665 672 if not fm.isplain():
666 673 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
667 674 added)
668 675 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
669 676 dropped)
670 677 fm.condwrite(ui.verbose, 'files_conflicting',
671 678 _('Files conflicting: %d\n'), conflicting)
General Comments 0
You need to be logged in to leave comments. Login now