##// END OF EJS Templates
scmutil: use new dirs class in dirstate and context...
Bryan O'Sullivan -
r18899:d8ff607e default
parent child Browse files
Show More
@@ -1,1380 +1,1371 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 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 node import nullid, nullrev, short, hex, bin
9 9 from i18n import _
10 10 import ancestor, mdiff, error, util, scmutil, subrepo, patch, encoding, phases
11 11 import copies
12 12 import match as matchmod
13 13 import os, errno, stat
14 14 import obsolete as obsmod
15 15 import repoview
16 16
17 17 propertycache = util.propertycache
18 18
19 19 class changectx(object):
20 20 """A changecontext object makes access to data related to a particular
21 21 changeset convenient."""
22 22 def __init__(self, repo, changeid=''):
23 23 """changeid is a revision number, node, or tag"""
24 24 if changeid == '':
25 25 changeid = '.'
26 26 self._repo = repo
27 27
28 28 if isinstance(changeid, int):
29 29 try:
30 30 self._node = repo.changelog.node(changeid)
31 31 except IndexError:
32 32 raise error.RepoLookupError(
33 33 _("unknown revision '%s'") % changeid)
34 34 self._rev = changeid
35 35 return
36 36 if isinstance(changeid, long):
37 37 changeid = str(changeid)
38 38 if changeid == '.':
39 39 self._node = repo.dirstate.p1()
40 40 self._rev = repo.changelog.rev(self._node)
41 41 return
42 42 if changeid == 'null':
43 43 self._node = nullid
44 44 self._rev = nullrev
45 45 return
46 46 if changeid == 'tip':
47 47 self._node = repo.changelog.tip()
48 48 self._rev = repo.changelog.rev(self._node)
49 49 return
50 50 if len(changeid) == 20:
51 51 try:
52 52 self._node = changeid
53 53 self._rev = repo.changelog.rev(changeid)
54 54 return
55 55 except LookupError:
56 56 pass
57 57
58 58 try:
59 59 r = int(changeid)
60 60 if str(r) != changeid:
61 61 raise ValueError
62 62 l = len(repo.changelog)
63 63 if r < 0:
64 64 r += l
65 65 if r < 0 or r >= l:
66 66 raise ValueError
67 67 self._rev = r
68 68 self._node = repo.changelog.node(r)
69 69 return
70 70 except (ValueError, OverflowError, IndexError):
71 71 pass
72 72
73 73 if len(changeid) == 40:
74 74 try:
75 75 self._node = bin(changeid)
76 76 self._rev = repo.changelog.rev(self._node)
77 77 return
78 78 except (TypeError, LookupError):
79 79 pass
80 80
81 81 if changeid in repo._bookmarks:
82 82 self._node = repo._bookmarks[changeid]
83 83 self._rev = repo.changelog.rev(self._node)
84 84 return
85 85 if changeid in repo._tagscache.tags:
86 86 self._node = repo._tagscache.tags[changeid]
87 87 self._rev = repo.changelog.rev(self._node)
88 88 return
89 89 try:
90 90 self._node = repo.branchtip(changeid)
91 91 self._rev = repo.changelog.rev(self._node)
92 92 return
93 93 except error.RepoLookupError:
94 94 pass
95 95
96 96 self._node = repo.changelog._partialmatch(changeid)
97 97 if self._node is not None:
98 98 self._rev = repo.changelog.rev(self._node)
99 99 return
100 100
101 101 # lookup failed
102 102 # check if it might have come from damaged dirstate
103 103 #
104 104 # XXX we could avoid the unfiltered if we had a recognizable exception
105 105 # for filtered changeset access
106 106 if changeid in repo.unfiltered().dirstate.parents():
107 107 raise error.Abort(_("working directory has unknown parent '%s'!")
108 108 % short(changeid))
109 109 try:
110 110 if len(changeid) == 20:
111 111 changeid = hex(changeid)
112 112 except TypeError:
113 113 pass
114 114 raise error.RepoLookupError(
115 115 _("unknown revision '%s'") % changeid)
116 116
117 117 def __str__(self):
118 118 return short(self.node())
119 119
120 120 def __int__(self):
121 121 return self.rev()
122 122
123 123 def __repr__(self):
124 124 return "<changectx %s>" % str(self)
125 125
126 126 def __hash__(self):
127 127 try:
128 128 return hash(self._rev)
129 129 except AttributeError:
130 130 return id(self)
131 131
132 132 def __eq__(self, other):
133 133 try:
134 134 return self._rev == other._rev
135 135 except AttributeError:
136 136 return False
137 137
138 138 def __ne__(self, other):
139 139 return not (self == other)
140 140
141 141 def __nonzero__(self):
142 142 return self._rev != nullrev
143 143
144 144 @propertycache
145 145 def _changeset(self):
146 146 return self._repo.changelog.read(self.rev())
147 147
148 148 @propertycache
149 149 def _manifest(self):
150 150 return self._repo.manifest.read(self._changeset[0])
151 151
152 152 @propertycache
153 153 def _manifestdelta(self):
154 154 return self._repo.manifest.readdelta(self._changeset[0])
155 155
156 156 @propertycache
157 157 def _parents(self):
158 158 p = self._repo.changelog.parentrevs(self._rev)
159 159 if p[1] == nullrev:
160 160 p = p[:-1]
161 161 return [changectx(self._repo, x) for x in p]
162 162
163 163 @propertycache
164 164 def substate(self):
165 165 return subrepo.state(self, self._repo.ui)
166 166
167 167 def __contains__(self, key):
168 168 return key in self._manifest
169 169
170 170 def __getitem__(self, key):
171 171 return self.filectx(key)
172 172
173 173 def __iter__(self):
174 174 for f in sorted(self._manifest):
175 175 yield f
176 176
177 177 def changeset(self):
178 178 return self._changeset
179 179 def manifest(self):
180 180 return self._manifest
181 181 def manifestnode(self):
182 182 return self._changeset[0]
183 183
184 184 def rev(self):
185 185 return self._rev
186 186 def node(self):
187 187 return self._node
188 188 def hex(self):
189 189 return hex(self._node)
190 190 def user(self):
191 191 return self._changeset[1]
192 192 def date(self):
193 193 return self._changeset[2]
194 194 def files(self):
195 195 return self._changeset[3]
196 196 def description(self):
197 197 return self._changeset[4]
198 198 def branch(self):
199 199 return encoding.tolocal(self._changeset[5].get("branch"))
200 200 def closesbranch(self):
201 201 return 'close' in self._changeset[5]
202 202 def extra(self):
203 203 return self._changeset[5]
204 204 def tags(self):
205 205 return self._repo.nodetags(self._node)
206 206 def bookmarks(self):
207 207 return self._repo.nodebookmarks(self._node)
208 208 def phase(self):
209 209 return self._repo._phasecache.phase(self._repo, self._rev)
210 210 def phasestr(self):
211 211 return phases.phasenames[self.phase()]
212 212 def mutable(self):
213 213 return self.phase() > phases.public
214 214 def hidden(self):
215 215 return self._rev in repoview.filterrevs(self._repo, 'visible')
216 216
217 217 def parents(self):
218 218 """return contexts for each parent changeset"""
219 219 return self._parents
220 220
221 221 def p1(self):
222 222 return self._parents[0]
223 223
224 224 def p2(self):
225 225 if len(self._parents) == 2:
226 226 return self._parents[1]
227 227 return changectx(self._repo, -1)
228 228
229 229 def children(self):
230 230 """return contexts for each child changeset"""
231 231 c = self._repo.changelog.children(self._node)
232 232 return [changectx(self._repo, x) for x in c]
233 233
234 234 def ancestors(self):
235 235 for a in self._repo.changelog.ancestors([self._rev]):
236 236 yield changectx(self._repo, a)
237 237
238 238 def descendants(self):
239 239 for d in self._repo.changelog.descendants([self._rev]):
240 240 yield changectx(self._repo, d)
241 241
242 242 def obsolete(self):
243 243 """True if the changeset is obsolete"""
244 244 return self.rev() in obsmod.getrevs(self._repo, 'obsolete')
245 245
246 246 def extinct(self):
247 247 """True if the changeset is extinct"""
248 248 return self.rev() in obsmod.getrevs(self._repo, 'extinct')
249 249
250 250 def unstable(self):
251 251 """True if the changeset is not obsolete but it's ancestor are"""
252 252 return self.rev() in obsmod.getrevs(self._repo, 'unstable')
253 253
254 254 def bumped(self):
255 255 """True if the changeset try to be a successor of a public changeset
256 256
257 257 Only non-public and non-obsolete changesets may be bumped.
258 258 """
259 259 return self.rev() in obsmod.getrevs(self._repo, 'bumped')
260 260
261 261 def divergent(self):
262 262 """Is a successors of a changeset with multiple possible successors set
263 263
264 264 Only non-public and non-obsolete changesets may be divergent.
265 265 """
266 266 return self.rev() in obsmod.getrevs(self._repo, 'divergent')
267 267
268 268 def troubled(self):
269 269 """True if the changeset is either unstable, bumped or divergent"""
270 270 return self.unstable() or self.bumped() or self.divergent()
271 271
272 272 def troubles(self):
273 273 """return the list of troubles affecting this changesets.
274 274
275 275 Troubles are returned as strings. possible values are:
276 276 - unstable,
277 277 - bumped,
278 278 - divergent.
279 279 """
280 280 troubles = []
281 281 if self.unstable():
282 282 troubles.append('unstable')
283 283 if self.bumped():
284 284 troubles.append('bumped')
285 285 if self.divergent():
286 286 troubles.append('divergent')
287 287 return troubles
288 288
289 289 def _fileinfo(self, path):
290 290 if '_manifest' in self.__dict__:
291 291 try:
292 292 return self._manifest[path], self._manifest.flags(path)
293 293 except KeyError:
294 294 raise error.ManifestLookupError(self._node, path,
295 295 _('not found in manifest'))
296 296 if '_manifestdelta' in self.__dict__ or path in self.files():
297 297 if path in self._manifestdelta:
298 298 return (self._manifestdelta[path],
299 299 self._manifestdelta.flags(path))
300 300 node, flag = self._repo.manifest.find(self._changeset[0], path)
301 301 if not node:
302 302 raise error.ManifestLookupError(self._node, path,
303 303 _('not found in manifest'))
304 304
305 305 return node, flag
306 306
307 307 def filenode(self, path):
308 308 return self._fileinfo(path)[0]
309 309
310 310 def flags(self, path):
311 311 try:
312 312 return self._fileinfo(path)[1]
313 313 except error.LookupError:
314 314 return ''
315 315
316 316 def filectx(self, path, fileid=None, filelog=None):
317 317 """get a file context from this changeset"""
318 318 if fileid is None:
319 319 fileid = self.filenode(path)
320 320 return filectx(self._repo, path, fileid=fileid,
321 321 changectx=self, filelog=filelog)
322 322
323 323 def ancestor(self, c2):
324 324 """
325 325 return the ancestor context of self and c2
326 326 """
327 327 # deal with workingctxs
328 328 n2 = c2._node
329 329 if n2 is None:
330 330 n2 = c2._parents[0]._node
331 331 n = self._repo.changelog.ancestor(self._node, n2)
332 332 return changectx(self._repo, n)
333 333
334 334 def descendant(self, other):
335 335 """True if other is descendant of this changeset"""
336 336 return self._repo.changelog.descendant(self._rev, other._rev)
337 337
338 338 def walk(self, match):
339 339 fset = set(match.files())
340 340 # for dirstate.walk, files=['.'] means "walk the whole tree".
341 341 # follow that here, too
342 342 fset.discard('.')
343 343 for fn in self:
344 344 if fn in fset:
345 345 # specified pattern is the exact name
346 346 fset.remove(fn)
347 347 if match(fn):
348 348 yield fn
349 349 for fn in sorted(fset):
350 350 if fn in self._dirs:
351 351 # specified pattern is a directory
352 352 continue
353 353 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
354 354 yield fn
355 355
356 356 def sub(self, path):
357 357 return subrepo.subrepo(self, path)
358 358
359 359 def match(self, pats=[], include=None, exclude=None, default='glob'):
360 360 r = self._repo
361 361 return matchmod.match(r.root, r.getcwd(), pats,
362 362 include, exclude, default,
363 363 auditor=r.auditor, ctx=self)
364 364
365 365 def diff(self, ctx2=None, match=None, **opts):
366 366 """Returns a diff generator for the given contexts and matcher"""
367 367 if ctx2 is None:
368 368 ctx2 = self.p1()
369 369 if ctx2 is not None and not isinstance(ctx2, changectx):
370 370 ctx2 = self._repo[ctx2]
371 371 diffopts = patch.diffopts(self._repo.ui, opts)
372 372 return patch.diff(self._repo, ctx2.node(), self.node(),
373 373 match=match, opts=diffopts)
374 374
375 375 @propertycache
376 376 def _dirs(self):
377 dirs = set()
378 for f in self._manifest:
379 pos = f.rfind('/')
380 while pos != -1:
381 f = f[:pos]
382 if f in dirs:
383 break # dirs already contains this and above
384 dirs.add(f)
385 pos = f.rfind('/')
386 return dirs
377 return scmutil.dirs(self._manifest)
387 378
388 379 def dirs(self):
389 380 return self._dirs
390 381
391 382 def dirty(self):
392 383 return False
393 384
394 385 class filectx(object):
395 386 """A filecontext object makes access to data related to a particular
396 387 filerevision convenient."""
397 388 def __init__(self, repo, path, changeid=None, fileid=None,
398 389 filelog=None, changectx=None):
399 390 """changeid can be a changeset revision, node, or tag.
400 391 fileid can be a file revision or node."""
401 392 self._repo = repo
402 393 self._path = path
403 394
404 395 assert (changeid is not None
405 396 or fileid is not None
406 397 or changectx is not None), \
407 398 ("bad args: changeid=%r, fileid=%r, changectx=%r"
408 399 % (changeid, fileid, changectx))
409 400
410 401 if filelog:
411 402 self._filelog = filelog
412 403
413 404 if changeid is not None:
414 405 self._changeid = changeid
415 406 if changectx is not None:
416 407 self._changectx = changectx
417 408 if fileid is not None:
418 409 self._fileid = fileid
419 410
420 411 @propertycache
421 412 def _changectx(self):
422 413 try:
423 414 return changectx(self._repo, self._changeid)
424 415 except error.RepoLookupError:
425 416 # Linkrev may point to any revision in the repository. When the
426 417 # repository is filtered this may lead to `filectx` trying to build
427 418 # `changectx` for filtered revision. In such case we fallback to
428 419 # creating `changectx` on the unfiltered version of the reposition.
429 420 # This fallback should not be an issue because `changectx` from
430 421 # `filectx` are not used in complex operations that care about
431 422 # filtering.
432 423 #
433 424 # This fallback is a cheap and dirty fix that prevent several
434 425 # crashes. It does not ensure the behavior is correct. However the
435 426 # behavior was not correct before filtering either and "incorrect
436 427 # behavior" is seen as better as "crash"
437 428 #
438 429 # Linkrevs have several serious troubles with filtering that are
439 430 # complicated to solve. Proper handling of the issue here should be
440 431 # considered when solving linkrev issue are on the table.
441 432 return changectx(self._repo.unfiltered(), self._changeid)
442 433
443 434 @propertycache
444 435 def _filelog(self):
445 436 return self._repo.file(self._path)
446 437
447 438 @propertycache
448 439 def _changeid(self):
449 440 if '_changectx' in self.__dict__:
450 441 return self._changectx.rev()
451 442 else:
452 443 return self._filelog.linkrev(self._filerev)
453 444
454 445 @propertycache
455 446 def _filenode(self):
456 447 if '_fileid' in self.__dict__:
457 448 return self._filelog.lookup(self._fileid)
458 449 else:
459 450 return self._changectx.filenode(self._path)
460 451
461 452 @propertycache
462 453 def _filerev(self):
463 454 return self._filelog.rev(self._filenode)
464 455
465 456 @propertycache
466 457 def _repopath(self):
467 458 return self._path
468 459
469 460 def __nonzero__(self):
470 461 try:
471 462 self._filenode
472 463 return True
473 464 except error.LookupError:
474 465 # file is missing
475 466 return False
476 467
477 468 def __str__(self):
478 469 return "%s@%s" % (self.path(), short(self.node()))
479 470
480 471 def __repr__(self):
481 472 return "<filectx %s>" % str(self)
482 473
483 474 def __hash__(self):
484 475 try:
485 476 return hash((self._path, self._filenode))
486 477 except AttributeError:
487 478 return id(self)
488 479
489 480 def __eq__(self, other):
490 481 try:
491 482 return (self._path == other._path
492 483 and self._filenode == other._filenode)
493 484 except AttributeError:
494 485 return False
495 486
496 487 def __ne__(self, other):
497 488 return not (self == other)
498 489
499 490 def filectx(self, fileid):
500 491 '''opens an arbitrary revision of the file without
501 492 opening a new filelog'''
502 493 return filectx(self._repo, self._path, fileid=fileid,
503 494 filelog=self._filelog)
504 495
505 496 def filerev(self):
506 497 return self._filerev
507 498 def filenode(self):
508 499 return self._filenode
509 500 def flags(self):
510 501 return self._changectx.flags(self._path)
511 502 def filelog(self):
512 503 return self._filelog
513 504
514 505 def rev(self):
515 506 if '_changectx' in self.__dict__:
516 507 return self._changectx.rev()
517 508 if '_changeid' in self.__dict__:
518 509 return self._changectx.rev()
519 510 return self._filelog.linkrev(self._filerev)
520 511
521 512 def linkrev(self):
522 513 return self._filelog.linkrev(self._filerev)
523 514 def node(self):
524 515 return self._changectx.node()
525 516 def hex(self):
526 517 return hex(self.node())
527 518 def user(self):
528 519 return self._changectx.user()
529 520 def date(self):
530 521 return self._changectx.date()
531 522 def files(self):
532 523 return self._changectx.files()
533 524 def description(self):
534 525 return self._changectx.description()
535 526 def branch(self):
536 527 return self._changectx.branch()
537 528 def extra(self):
538 529 return self._changectx.extra()
539 530 def phase(self):
540 531 return self._changectx.phase()
541 532 def phasestr(self):
542 533 return self._changectx.phasestr()
543 534 def manifest(self):
544 535 return self._changectx.manifest()
545 536 def changectx(self):
546 537 return self._changectx
547 538
548 539 def data(self):
549 540 return self._filelog.read(self._filenode)
550 541 def path(self):
551 542 return self._path
552 543 def size(self):
553 544 return self._filelog.size(self._filerev)
554 545
555 546 def isbinary(self):
556 547 try:
557 548 return util.binary(self.data())
558 549 except IOError:
559 550 return False
560 551
561 552 def cmp(self, fctx):
562 553 """compare with other file context
563 554
564 555 returns True if different than fctx.
565 556 """
566 557 if (fctx._filerev is None
567 558 and (self._repo._encodefilterpats
568 559 # if file data starts with '\1\n', empty metadata block is
569 560 # prepended, which adds 4 bytes to filelog.size().
570 561 or self.size() - 4 == fctx.size())
571 562 or self.size() == fctx.size()):
572 563 return self._filelog.cmp(self._filenode, fctx.data())
573 564
574 565 return True
575 566
576 567 def renamed(self):
577 568 """check if file was actually renamed in this changeset revision
578 569
579 570 If rename logged in file revision, we report copy for changeset only
580 571 if file revisions linkrev points back to the changeset in question
581 572 or both changeset parents contain different file revisions.
582 573 """
583 574
584 575 renamed = self._filelog.renamed(self._filenode)
585 576 if not renamed:
586 577 return renamed
587 578
588 579 if self.rev() == self.linkrev():
589 580 return renamed
590 581
591 582 name = self.path()
592 583 fnode = self._filenode
593 584 for p in self._changectx.parents():
594 585 try:
595 586 if fnode == p.filenode(name):
596 587 return None
597 588 except error.LookupError:
598 589 pass
599 590 return renamed
600 591
601 592 def parents(self):
602 593 p = self._path
603 594 fl = self._filelog
604 595 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
605 596
606 597 r = self._filelog.renamed(self._filenode)
607 598 if r:
608 599 pl[0] = (r[0], r[1], None)
609 600
610 601 return [filectx(self._repo, p, fileid=n, filelog=l)
611 602 for p, n, l in pl if n != nullid]
612 603
613 604 def p1(self):
614 605 return self.parents()[0]
615 606
616 607 def p2(self):
617 608 p = self.parents()
618 609 if len(p) == 2:
619 610 return p[1]
620 611 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
621 612
622 613 def children(self):
623 614 # hard for renames
624 615 c = self._filelog.children(self._filenode)
625 616 return [filectx(self._repo, self._path, fileid=x,
626 617 filelog=self._filelog) for x in c]
627 618
628 619 def annotate(self, follow=False, linenumber=None, diffopts=None):
629 620 '''returns a list of tuples of (ctx, line) for each line
630 621 in the file, where ctx is the filectx of the node where
631 622 that line was last changed.
632 623 This returns tuples of ((ctx, linenumber), line) for each line,
633 624 if "linenumber" parameter is NOT "None".
634 625 In such tuples, linenumber means one at the first appearance
635 626 in the managed file.
636 627 To reduce annotation cost,
637 628 this returns fixed value(False is used) as linenumber,
638 629 if "linenumber" parameter is "False".'''
639 630
640 631 def decorate_compat(text, rev):
641 632 return ([rev] * len(text.splitlines()), text)
642 633
643 634 def without_linenumber(text, rev):
644 635 return ([(rev, False)] * len(text.splitlines()), text)
645 636
646 637 def with_linenumber(text, rev):
647 638 size = len(text.splitlines())
648 639 return ([(rev, i) for i in xrange(1, size + 1)], text)
649 640
650 641 decorate = (((linenumber is None) and decorate_compat) or
651 642 (linenumber and with_linenumber) or
652 643 without_linenumber)
653 644
654 645 def pair(parent, child):
655 646 blocks = mdiff.allblocks(parent[1], child[1], opts=diffopts,
656 647 refine=True)
657 648 for (a1, a2, b1, b2), t in blocks:
658 649 # Changed blocks ('!') or blocks made only of blank lines ('~')
659 650 # belong to the child.
660 651 if t == '=':
661 652 child[0][b1:b2] = parent[0][a1:a2]
662 653 return child
663 654
664 655 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
665 656 def getctx(path, fileid):
666 657 log = path == self._path and self._filelog or getlog(path)
667 658 return filectx(self._repo, path, fileid=fileid, filelog=log)
668 659 getctx = util.lrucachefunc(getctx)
669 660
670 661 def parents(f):
671 662 # we want to reuse filectx objects as much as possible
672 663 p = f._path
673 664 if f._filerev is None: # working dir
674 665 pl = [(n.path(), n.filerev()) for n in f.parents()]
675 666 else:
676 667 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
677 668
678 669 if follow:
679 670 r = f.renamed()
680 671 if r:
681 672 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
682 673
683 674 return [getctx(p, n) for p, n in pl if n != nullrev]
684 675
685 676 # use linkrev to find the first changeset where self appeared
686 677 if self.rev() != self.linkrev():
687 678 base = self.filectx(self.filerev())
688 679 else:
689 680 base = self
690 681
691 682 # This algorithm would prefer to be recursive, but Python is a
692 683 # bit recursion-hostile. Instead we do an iterative
693 684 # depth-first search.
694 685
695 686 visit = [base]
696 687 hist = {}
697 688 pcache = {}
698 689 needed = {base: 1}
699 690 while visit:
700 691 f = visit[-1]
701 692 if f not in pcache:
702 693 pcache[f] = parents(f)
703 694
704 695 ready = True
705 696 pl = pcache[f]
706 697 for p in pl:
707 698 if p not in hist:
708 699 ready = False
709 700 visit.append(p)
710 701 needed[p] = needed.get(p, 0) + 1
711 702 if ready:
712 703 visit.pop()
713 704 curr = decorate(f.data(), f)
714 705 for p in pl:
715 706 curr = pair(hist[p], curr)
716 707 if needed[p] == 1:
717 708 del hist[p]
718 709 else:
719 710 needed[p] -= 1
720 711
721 712 hist[f] = curr
722 713 pcache[f] = []
723 714
724 715 return zip(hist[base][0], hist[base][1].splitlines(True))
725 716
726 717 def ancestor(self, fc2, actx):
727 718 """
728 719 find the common ancestor file context, if any, of self, and fc2
729 720
730 721 actx must be the changectx of the common ancestor
731 722 of self's and fc2's respective changesets.
732 723 """
733 724
734 725 # the easy case: no (relevant) renames
735 726 if fc2.path() == self.path() and self.path() in actx:
736 727 return actx[self.path()]
737 728
738 729 # the next easiest cases: unambiguous predecessor (name trumps
739 730 # history)
740 731 if self.path() in actx and fc2.path() not in actx:
741 732 return actx[self.path()]
742 733 if fc2.path() in actx and self.path() not in actx:
743 734 return actx[fc2.path()]
744 735
745 736 # prime the ancestor cache for the working directory
746 737 acache = {}
747 738 for c in (self, fc2):
748 739 if c._filerev is None:
749 740 pl = [(n.path(), n.filenode()) for n in c.parents()]
750 741 acache[(c._path, None)] = pl
751 742
752 743 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
753 744 def parents(vertex):
754 745 if vertex in acache:
755 746 return acache[vertex]
756 747 f, n = vertex
757 748 if f not in flcache:
758 749 flcache[f] = self._repo.file(f)
759 750 fl = flcache[f]
760 751 pl = [(f, p) for p in fl.parents(n) if p != nullid]
761 752 re = fl.renamed(n)
762 753 if re:
763 754 pl.append(re)
764 755 acache[vertex] = pl
765 756 return pl
766 757
767 758 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
768 759 v = ancestor.ancestor(a, b, parents)
769 760 if v:
770 761 f, n = v
771 762 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
772 763
773 764 return None
774 765
775 766 def ancestors(self, followfirst=False):
776 767 visit = {}
777 768 c = self
778 769 cut = followfirst and 1 or None
779 770 while True:
780 771 for parent in c.parents()[:cut]:
781 772 visit[(parent.rev(), parent.node())] = parent
782 773 if not visit:
783 774 break
784 775 c = visit.pop(max(visit))
785 776 yield c
786 777
787 778 def copies(self, c2):
788 779 if not util.safehasattr(self, "_copycache"):
789 780 self._copycache = {}
790 781 sc2 = str(c2)
791 782 if sc2 not in self._copycache:
792 783 self._copycache[sc2] = copies.pathcopies(c2)
793 784 return self._copycache[sc2]
794 785
795 786 class workingctx(changectx):
796 787 """A workingctx object makes access to data related to
797 788 the current working directory convenient.
798 789 date - any valid date string or (unixtime, offset), or None.
799 790 user - username string, or None.
800 791 extra - a dictionary of extra values, or None.
801 792 changes - a list of file lists as returned by localrepo.status()
802 793 or None to use the repository status.
803 794 """
804 795 def __init__(self, repo, text="", user=None, date=None, extra=None,
805 796 changes=None):
806 797 self._repo = repo
807 798 self._rev = None
808 799 self._node = None
809 800 self._text = text
810 801 if date:
811 802 self._date = util.parsedate(date)
812 803 if user:
813 804 self._user = user
814 805 if changes:
815 806 self._status = list(changes[:4])
816 807 self._unknown = changes[4]
817 808 self._ignored = changes[5]
818 809 self._clean = changes[6]
819 810 else:
820 811 self._unknown = None
821 812 self._ignored = None
822 813 self._clean = None
823 814
824 815 self._extra = {}
825 816 if extra:
826 817 self._extra = extra.copy()
827 818 if 'branch' not in self._extra:
828 819 try:
829 820 branch = encoding.fromlocal(self._repo.dirstate.branch())
830 821 except UnicodeDecodeError:
831 822 raise util.Abort(_('branch name not in UTF-8!'))
832 823 self._extra['branch'] = branch
833 824 if self._extra['branch'] == '':
834 825 self._extra['branch'] = 'default'
835 826
836 827 def __str__(self):
837 828 return str(self._parents[0]) + "+"
838 829
839 830 def __repr__(self):
840 831 return "<workingctx %s>" % str(self)
841 832
842 833 def __nonzero__(self):
843 834 return True
844 835
845 836 def __contains__(self, key):
846 837 return self._repo.dirstate[key] not in "?r"
847 838
848 839 def _buildflagfunc(self):
849 840 # Create a fallback function for getting file flags when the
850 841 # filesystem doesn't support them
851 842
852 843 copiesget = self._repo.dirstate.copies().get
853 844
854 845 if len(self._parents) < 2:
855 846 # when we have one parent, it's easy: copy from parent
856 847 man = self._parents[0].manifest()
857 848 def func(f):
858 849 f = copiesget(f, f)
859 850 return man.flags(f)
860 851 else:
861 852 # merges are tricky: we try to reconstruct the unstored
862 853 # result from the merge (issue1802)
863 854 p1, p2 = self._parents
864 855 pa = p1.ancestor(p2)
865 856 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
866 857
867 858 def func(f):
868 859 f = copiesget(f, f) # may be wrong for merges with copies
869 860 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
870 861 if fl1 == fl2:
871 862 return fl1
872 863 if fl1 == fla:
873 864 return fl2
874 865 if fl2 == fla:
875 866 return fl1
876 867 return '' # punt for conflicts
877 868
878 869 return func
879 870
880 871 @propertycache
881 872 def _flagfunc(self):
882 873 return self._repo.dirstate.flagfunc(self._buildflagfunc)
883 874
884 875 @propertycache
885 876 def _manifest(self):
886 877 """generate a manifest corresponding to the working directory"""
887 878
888 879 man = self._parents[0].manifest().copy()
889 880 if len(self._parents) > 1:
890 881 man2 = self.p2().manifest()
891 882 def getman(f):
892 883 if f in man:
893 884 return man
894 885 return man2
895 886 else:
896 887 getman = lambda f: man
897 888
898 889 copied = self._repo.dirstate.copies()
899 890 ff = self._flagfunc
900 891 modified, added, removed, deleted = self._status
901 892 for i, l in (("a", added), ("m", modified)):
902 893 for f in l:
903 894 orig = copied.get(f, f)
904 895 man[f] = getman(orig).get(orig, nullid) + i
905 896 try:
906 897 man.set(f, ff(f))
907 898 except OSError:
908 899 pass
909 900
910 901 for f in deleted + removed:
911 902 if f in man:
912 903 del man[f]
913 904
914 905 return man
915 906
916 907 def __iter__(self):
917 908 d = self._repo.dirstate
918 909 for f in d:
919 910 if d[f] != 'r':
920 911 yield f
921 912
922 913 @propertycache
923 914 def _status(self):
924 915 return self._repo.status()[:4]
925 916
926 917 @propertycache
927 918 def _user(self):
928 919 return self._repo.ui.username()
929 920
930 921 @propertycache
931 922 def _date(self):
932 923 return util.makedate()
933 924
934 925 @propertycache
935 926 def _parents(self):
936 927 p = self._repo.dirstate.parents()
937 928 if p[1] == nullid:
938 929 p = p[:-1]
939 930 return [changectx(self._repo, x) for x in p]
940 931
941 932 def status(self, ignored=False, clean=False, unknown=False):
942 933 """Explicit status query
943 934 Unless this method is used to query the working copy status, the
944 935 _status property will implicitly read the status using its default
945 936 arguments."""
946 937 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
947 938 self._unknown = self._ignored = self._clean = None
948 939 if unknown:
949 940 self._unknown = stat[4]
950 941 if ignored:
951 942 self._ignored = stat[5]
952 943 if clean:
953 944 self._clean = stat[6]
954 945 self._status = stat[:4]
955 946 return stat
956 947
957 948 def manifest(self):
958 949 return self._manifest
959 950 def user(self):
960 951 return self._user or self._repo.ui.username()
961 952 def date(self):
962 953 return self._date
963 954 def description(self):
964 955 return self._text
965 956 def files(self):
966 957 return sorted(self._status[0] + self._status[1] + self._status[2])
967 958
968 959 def modified(self):
969 960 return self._status[0]
970 961 def added(self):
971 962 return self._status[1]
972 963 def removed(self):
973 964 return self._status[2]
974 965 def deleted(self):
975 966 return self._status[3]
976 967 def unknown(self):
977 968 assert self._unknown is not None # must call status first
978 969 return self._unknown
979 970 def ignored(self):
980 971 assert self._ignored is not None # must call status first
981 972 return self._ignored
982 973 def clean(self):
983 974 assert self._clean is not None # must call status first
984 975 return self._clean
985 976 def branch(self):
986 977 return encoding.tolocal(self._extra['branch'])
987 978 def closesbranch(self):
988 979 return 'close' in self._extra
989 980 def extra(self):
990 981 return self._extra
991 982
992 983 def tags(self):
993 984 t = []
994 985 for p in self.parents():
995 986 t.extend(p.tags())
996 987 return t
997 988
998 989 def bookmarks(self):
999 990 b = []
1000 991 for p in self.parents():
1001 992 b.extend(p.bookmarks())
1002 993 return b
1003 994
1004 995 def phase(self):
1005 996 phase = phases.draft # default phase to draft
1006 997 for p in self.parents():
1007 998 phase = max(phase, p.phase())
1008 999 return phase
1009 1000
1010 1001 def hidden(self):
1011 1002 return False
1012 1003
1013 1004 def children(self):
1014 1005 return []
1015 1006
1016 1007 def flags(self, path):
1017 1008 if '_manifest' in self.__dict__:
1018 1009 try:
1019 1010 return self._manifest.flags(path)
1020 1011 except KeyError:
1021 1012 return ''
1022 1013
1023 1014 try:
1024 1015 return self._flagfunc(path)
1025 1016 except OSError:
1026 1017 return ''
1027 1018
1028 1019 def filectx(self, path, filelog=None):
1029 1020 """get a file context from the working directory"""
1030 1021 return workingfilectx(self._repo, path, workingctx=self,
1031 1022 filelog=filelog)
1032 1023
1033 1024 def ancestor(self, c2):
1034 1025 """return the ancestor context of self and c2"""
1035 1026 return self._parents[0].ancestor(c2) # punt on two parents for now
1036 1027
1037 1028 def walk(self, match):
1038 1029 return sorted(self._repo.dirstate.walk(match, sorted(self.substate),
1039 1030 True, False))
1040 1031
1041 1032 def dirty(self, missing=False, merge=True, branch=True):
1042 1033 "check whether a working directory is modified"
1043 1034 # check subrepos first
1044 1035 for s in sorted(self.substate):
1045 1036 if self.sub(s).dirty():
1046 1037 return True
1047 1038 # check current working dir
1048 1039 return ((merge and self.p2()) or
1049 1040 (branch and self.branch() != self.p1().branch()) or
1050 1041 self.modified() or self.added() or self.removed() or
1051 1042 (missing and self.deleted()))
1052 1043
1053 1044 def add(self, list, prefix=""):
1054 1045 join = lambda f: os.path.join(prefix, f)
1055 1046 wlock = self._repo.wlock()
1056 1047 ui, ds = self._repo.ui, self._repo.dirstate
1057 1048 try:
1058 1049 rejected = []
1059 1050 for f in list:
1060 1051 scmutil.checkportable(ui, join(f))
1061 1052 p = self._repo.wjoin(f)
1062 1053 try:
1063 1054 st = os.lstat(p)
1064 1055 except OSError:
1065 1056 ui.warn(_("%s does not exist!\n") % join(f))
1066 1057 rejected.append(f)
1067 1058 continue
1068 1059 if st.st_size > 10000000:
1069 1060 ui.warn(_("%s: up to %d MB of RAM may be required "
1070 1061 "to manage this file\n"
1071 1062 "(use 'hg revert %s' to cancel the "
1072 1063 "pending addition)\n")
1073 1064 % (f, 3 * st.st_size // 1000000, join(f)))
1074 1065 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1075 1066 ui.warn(_("%s not added: only files and symlinks "
1076 1067 "supported currently\n") % join(f))
1077 1068 rejected.append(p)
1078 1069 elif ds[f] in 'amn':
1079 1070 ui.warn(_("%s already tracked!\n") % join(f))
1080 1071 elif ds[f] == 'r':
1081 1072 ds.normallookup(f)
1082 1073 else:
1083 1074 ds.add(f)
1084 1075 return rejected
1085 1076 finally:
1086 1077 wlock.release()
1087 1078
1088 1079 def forget(self, files, prefix=""):
1089 1080 join = lambda f: os.path.join(prefix, f)
1090 1081 wlock = self._repo.wlock()
1091 1082 try:
1092 1083 rejected = []
1093 1084 for f in files:
1094 1085 if f not in self._repo.dirstate:
1095 1086 self._repo.ui.warn(_("%s not tracked!\n") % join(f))
1096 1087 rejected.append(f)
1097 1088 elif self._repo.dirstate[f] != 'a':
1098 1089 self._repo.dirstate.remove(f)
1099 1090 else:
1100 1091 self._repo.dirstate.drop(f)
1101 1092 return rejected
1102 1093 finally:
1103 1094 wlock.release()
1104 1095
1105 1096 def ancestors(self):
1106 1097 for a in self._repo.changelog.ancestors(
1107 1098 [p.rev() for p in self._parents]):
1108 1099 yield changectx(self._repo, a)
1109 1100
1110 1101 def undelete(self, list):
1111 1102 pctxs = self.parents()
1112 1103 wlock = self._repo.wlock()
1113 1104 try:
1114 1105 for f in list:
1115 1106 if self._repo.dirstate[f] != 'r':
1116 1107 self._repo.ui.warn(_("%s not removed!\n") % f)
1117 1108 else:
1118 1109 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
1119 1110 t = fctx.data()
1120 1111 self._repo.wwrite(f, t, fctx.flags())
1121 1112 self._repo.dirstate.normal(f)
1122 1113 finally:
1123 1114 wlock.release()
1124 1115
1125 1116 def copy(self, source, dest):
1126 1117 p = self._repo.wjoin(dest)
1127 1118 if not os.path.lexists(p):
1128 1119 self._repo.ui.warn(_("%s does not exist!\n") % dest)
1129 1120 elif not (os.path.isfile(p) or os.path.islink(p)):
1130 1121 self._repo.ui.warn(_("copy failed: %s is not a file or a "
1131 1122 "symbolic link\n") % dest)
1132 1123 else:
1133 1124 wlock = self._repo.wlock()
1134 1125 try:
1135 1126 if self._repo.dirstate[dest] in '?r':
1136 1127 self._repo.dirstate.add(dest)
1137 1128 self._repo.dirstate.copy(source, dest)
1138 1129 finally:
1139 1130 wlock.release()
1140 1131
1141 1132 def markcommitted(self, node):
1142 1133 """Perform post-commit cleanup necessary after commiting this workingctx
1143 1134
1144 1135 Specifically, this updates backing stores this working context
1145 1136 wraps to reflect the fact that the changes reflected by this
1146 1137 workingctx have been committed. For example, it marks
1147 1138 modified and added files as normal in the dirstate.
1148 1139
1149 1140 """
1150 1141
1151 1142 for f in self.modified() + self.added():
1152 1143 self._repo.dirstate.normal(f)
1153 1144 for f in self.removed():
1154 1145 self._repo.dirstate.drop(f)
1155 1146 self._repo.dirstate.setparents(node)
1156 1147
1157 1148 def dirs(self):
1158 return set(self._repo.dirstate.dirs())
1149 return self._repo.dirstate.dirs()
1159 1150
1160 1151 class workingfilectx(filectx):
1161 1152 """A workingfilectx object makes access to data related to a particular
1162 1153 file in the working directory convenient."""
1163 1154 def __init__(self, repo, path, filelog=None, workingctx=None):
1164 1155 """changeid can be a changeset revision, node, or tag.
1165 1156 fileid can be a file revision or node."""
1166 1157 self._repo = repo
1167 1158 self._path = path
1168 1159 self._changeid = None
1169 1160 self._filerev = self._filenode = None
1170 1161
1171 1162 if filelog:
1172 1163 self._filelog = filelog
1173 1164 if workingctx:
1174 1165 self._changectx = workingctx
1175 1166
1176 1167 @propertycache
1177 1168 def _changectx(self):
1178 1169 return workingctx(self._repo)
1179 1170
1180 1171 def __nonzero__(self):
1181 1172 return True
1182 1173
1183 1174 def __str__(self):
1184 1175 return "%s@%s" % (self.path(), self._changectx)
1185 1176
1186 1177 def __repr__(self):
1187 1178 return "<workingfilectx %s>" % str(self)
1188 1179
1189 1180 def data(self):
1190 1181 return self._repo.wread(self._path)
1191 1182 def renamed(self):
1192 1183 rp = self._repo.dirstate.copied(self._path)
1193 1184 if not rp:
1194 1185 return None
1195 1186 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
1196 1187
1197 1188 def parents(self):
1198 1189 '''return parent filectxs, following copies if necessary'''
1199 1190 def filenode(ctx, path):
1200 1191 return ctx._manifest.get(path, nullid)
1201 1192
1202 1193 path = self._path
1203 1194 fl = self._filelog
1204 1195 pcl = self._changectx._parents
1205 1196 renamed = self.renamed()
1206 1197
1207 1198 if renamed:
1208 1199 pl = [renamed + (None,)]
1209 1200 else:
1210 1201 pl = [(path, filenode(pcl[0], path), fl)]
1211 1202
1212 1203 for pc in pcl[1:]:
1213 1204 pl.append((path, filenode(pc, path), fl))
1214 1205
1215 1206 return [filectx(self._repo, p, fileid=n, filelog=l)
1216 1207 for p, n, l in pl if n != nullid]
1217 1208
1218 1209 def children(self):
1219 1210 return []
1220 1211
1221 1212 def size(self):
1222 1213 return os.lstat(self._repo.wjoin(self._path)).st_size
1223 1214 def date(self):
1224 1215 t, tz = self._changectx.date()
1225 1216 try:
1226 1217 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
1227 1218 except OSError, err:
1228 1219 if err.errno != errno.ENOENT:
1229 1220 raise
1230 1221 return (t, tz)
1231 1222
1232 1223 def cmp(self, fctx):
1233 1224 """compare with other file context
1234 1225
1235 1226 returns True if different than fctx.
1236 1227 """
1237 1228 # fctx should be a filectx (not a workingfilectx)
1238 1229 # invert comparison to reuse the same code path
1239 1230 return fctx.cmp(self)
1240 1231
1241 1232 class memctx(object):
1242 1233 """Use memctx to perform in-memory commits via localrepo.commitctx().
1243 1234
1244 1235 Revision information is supplied at initialization time while
1245 1236 related files data and is made available through a callback
1246 1237 mechanism. 'repo' is the current localrepo, 'parents' is a
1247 1238 sequence of two parent revisions identifiers (pass None for every
1248 1239 missing parent), 'text' is the commit message and 'files' lists
1249 1240 names of files touched by the revision (normalized and relative to
1250 1241 repository root).
1251 1242
1252 1243 filectxfn(repo, memctx, path) is a callable receiving the
1253 1244 repository, the current memctx object and the normalized path of
1254 1245 requested file, relative to repository root. It is fired by the
1255 1246 commit function for every file in 'files', but calls order is
1256 1247 undefined. If the file is available in the revision being
1257 1248 committed (updated or added), filectxfn returns a memfilectx
1258 1249 object. If the file was removed, filectxfn raises an
1259 1250 IOError. Moved files are represented by marking the source file
1260 1251 removed and the new file added with copy information (see
1261 1252 memfilectx).
1262 1253
1263 1254 user receives the committer name and defaults to current
1264 1255 repository username, date is the commit date in any format
1265 1256 supported by util.parsedate() and defaults to current date, extra
1266 1257 is a dictionary of metadata or is left empty.
1267 1258 """
1268 1259 def __init__(self, repo, parents, text, files, filectxfn, user=None,
1269 1260 date=None, extra=None):
1270 1261 self._repo = repo
1271 1262 self._rev = None
1272 1263 self._node = None
1273 1264 self._text = text
1274 1265 self._date = date and util.parsedate(date) or util.makedate()
1275 1266 self._user = user
1276 1267 parents = [(p or nullid) for p in parents]
1277 1268 p1, p2 = parents
1278 1269 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
1279 1270 files = sorted(set(files))
1280 1271 self._status = [files, [], [], [], []]
1281 1272 self._filectxfn = filectxfn
1282 1273
1283 1274 self._extra = extra and extra.copy() or {}
1284 1275 if self._extra.get('branch', '') == '':
1285 1276 self._extra['branch'] = 'default'
1286 1277
1287 1278 def __str__(self):
1288 1279 return str(self._parents[0]) + "+"
1289 1280
1290 1281 def __int__(self):
1291 1282 return self._rev
1292 1283
1293 1284 def __nonzero__(self):
1294 1285 return True
1295 1286
1296 1287 def __getitem__(self, key):
1297 1288 return self.filectx(key)
1298 1289
1299 1290 def p1(self):
1300 1291 return self._parents[0]
1301 1292 def p2(self):
1302 1293 return self._parents[1]
1303 1294
1304 1295 def user(self):
1305 1296 return self._user or self._repo.ui.username()
1306 1297 def date(self):
1307 1298 return self._date
1308 1299 def description(self):
1309 1300 return self._text
1310 1301 def files(self):
1311 1302 return self.modified()
1312 1303 def modified(self):
1313 1304 return self._status[0]
1314 1305 def added(self):
1315 1306 return self._status[1]
1316 1307 def removed(self):
1317 1308 return self._status[2]
1318 1309 def deleted(self):
1319 1310 return self._status[3]
1320 1311 def unknown(self):
1321 1312 return self._status[4]
1322 1313 def ignored(self):
1323 1314 return self._status[5]
1324 1315 def clean(self):
1325 1316 return self._status[6]
1326 1317 def branch(self):
1327 1318 return encoding.tolocal(self._extra['branch'])
1328 1319 def extra(self):
1329 1320 return self._extra
1330 1321 def flags(self, f):
1331 1322 return self[f].flags()
1332 1323
1333 1324 def parents(self):
1334 1325 """return contexts for each parent changeset"""
1335 1326 return self._parents
1336 1327
1337 1328 def filectx(self, path, filelog=None):
1338 1329 """get a file context from the working directory"""
1339 1330 return self._filectxfn(self._repo, self, path)
1340 1331
1341 1332 def commit(self):
1342 1333 """commit context to the repo"""
1343 1334 return self._repo.commitctx(self)
1344 1335
1345 1336 class memfilectx(object):
1346 1337 """memfilectx represents an in-memory file to commit.
1347 1338
1348 1339 See memctx for more details.
1349 1340 """
1350 1341 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1351 1342 """
1352 1343 path is the normalized file path relative to repository root.
1353 1344 data is the file content as a string.
1354 1345 islink is True if the file is a symbolic link.
1355 1346 isexec is True if the file is executable.
1356 1347 copied is the source file path if current file was copied in the
1357 1348 revision being committed, or None."""
1358 1349 self._path = path
1359 1350 self._data = data
1360 1351 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1361 1352 self._copied = None
1362 1353 if copied:
1363 1354 self._copied = (copied, nullid)
1364 1355
1365 1356 def __nonzero__(self):
1366 1357 return True
1367 1358 def __str__(self):
1368 1359 return "%s@%s" % (self.path(), self._changectx)
1369 1360 def path(self):
1370 1361 return self._path
1371 1362 def data(self):
1372 1363 return self._data
1373 1364 def flags(self):
1374 1365 return self._flags
1375 1366 def isexec(self):
1376 1367 return 'x' in self._flags
1377 1368 def islink(self):
1378 1369 return 'l' in self._flags
1379 1370 def renamed(self):
1380 1371 return self._copied
@@ -1,388 +1,388 b''
1 1 # copies.py - copy detection for Mercurial
2 2 #
3 3 # Copyright 2008 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 import util
9 9 import heapq
10 10
11 11 def _nonoverlap(d1, d2, d3):
12 12 "Return list of elements in d1 not in d2 or d3"
13 13 return sorted([d for d in d1 if d not in d3 and d not in d2])
14 14
15 15 def _dirname(f):
16 16 s = f.rfind("/")
17 17 if s == -1:
18 18 return ""
19 19 return f[:s]
20 20
21 21 def _findlimit(repo, a, b):
22 22 """Find the earliest revision that's an ancestor of a or b but not both,
23 23 None if no such revision exists.
24 24 """
25 25 # basic idea:
26 26 # - mark a and b with different sides
27 27 # - if a parent's children are all on the same side, the parent is
28 28 # on that side, otherwise it is on no side
29 29 # - walk the graph in topological order with the help of a heap;
30 30 # - add unseen parents to side map
31 31 # - clear side of any parent that has children on different sides
32 32 # - track number of interesting revs that might still be on a side
33 33 # - track the lowest interesting rev seen
34 34 # - quit when interesting revs is zero
35 35
36 36 cl = repo.changelog
37 37 working = len(cl) # pseudo rev for the working directory
38 38 if a is None:
39 39 a = working
40 40 if b is None:
41 41 b = working
42 42
43 43 side = {a: -1, b: 1}
44 44 visit = [-a, -b]
45 45 heapq.heapify(visit)
46 46 interesting = len(visit)
47 47 hascommonancestor = False
48 48 limit = working
49 49
50 50 while interesting:
51 51 r = -heapq.heappop(visit)
52 52 if r == working:
53 53 parents = [cl.rev(p) for p in repo.dirstate.parents()]
54 54 else:
55 55 parents = cl.parentrevs(r)
56 56 for p in parents:
57 57 if p < 0:
58 58 continue
59 59 if p not in side:
60 60 # first time we see p; add it to visit
61 61 side[p] = side[r]
62 62 if side[p]:
63 63 interesting += 1
64 64 heapq.heappush(visit, -p)
65 65 elif side[p] and side[p] != side[r]:
66 66 # p was interesting but now we know better
67 67 side[p] = 0
68 68 interesting -= 1
69 69 hascommonancestor = True
70 70 if side[r]:
71 71 limit = r # lowest rev visited
72 72 interesting -= 1
73 73
74 74 if not hascommonancestor:
75 75 return None
76 76 return limit
77 77
78 78 def _chain(src, dst, a, b):
79 79 '''chain two sets of copies a->b'''
80 80 t = a.copy()
81 81 for k, v in b.iteritems():
82 82 if v in t:
83 83 # found a chain
84 84 if t[v] != k:
85 85 # file wasn't renamed back to itself
86 86 t[k] = t[v]
87 87 if v not in dst:
88 88 # chain was a rename, not a copy
89 89 del t[v]
90 90 if v in src:
91 91 # file is a copy of an existing file
92 92 t[k] = v
93 93
94 94 # remove criss-crossed copies
95 95 for k, v in t.items():
96 96 if k in src and v in dst:
97 97 del t[k]
98 98
99 99 return t
100 100
101 101 def _tracefile(fctx, actx):
102 102 '''return file context that is the ancestor of fctx present in actx'''
103 103 stop = actx.rev()
104 104 am = actx.manifest()
105 105
106 106 for f in fctx.ancestors():
107 107 if am.get(f.path(), None) == f.filenode():
108 108 return f
109 109 if f.rev() < stop:
110 110 return None
111 111
112 112 def _dirstatecopies(d):
113 113 ds = d._repo.dirstate
114 114 c = ds.copies().copy()
115 115 for k in c.keys():
116 116 if ds[k] not in 'anm':
117 117 del c[k]
118 118 return c
119 119
120 120 def _forwardcopies(a, b):
121 121 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
122 122
123 123 # check for working copy
124 124 w = None
125 125 if b.rev() is None:
126 126 w = b
127 127 b = w.p1()
128 128 if a == b:
129 129 # short-circuit to avoid issues with merge states
130 130 return _dirstatecopies(w)
131 131
132 132 # find where new files came from
133 133 # we currently don't try to find where old files went, too expensive
134 134 # this means we can miss a case like 'hg rm b; hg cp a b'
135 135 cm = {}
136 136 missing = set(b.manifest().iterkeys())
137 137 missing.difference_update(a.manifest().iterkeys())
138 138
139 139 for f in missing:
140 140 ofctx = _tracefile(b[f], a)
141 141 if ofctx:
142 142 cm[f] = ofctx.path()
143 143
144 144 # combine copies from dirstate if necessary
145 145 if w is not None:
146 146 cm = _chain(a, w, cm, _dirstatecopies(w))
147 147
148 148 return cm
149 149
150 150 def _backwardrenames(a, b):
151 151 # Even though we're not taking copies into account, 1:n rename situations
152 152 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
153 153 # arbitrarily pick one of the renames.
154 154 f = _forwardcopies(b, a)
155 155 r = {}
156 156 for k, v in sorted(f.iteritems()):
157 157 # remove copies
158 158 if v in a:
159 159 continue
160 160 r[v] = k
161 161 return r
162 162
163 163 def pathcopies(x, y):
164 164 '''find {dst@y: src@x} copy mapping for directed compare'''
165 165 if x == y or not x or not y:
166 166 return {}
167 167 a = y.ancestor(x)
168 168 if a == x:
169 169 return _forwardcopies(x, y)
170 170 if a == y:
171 171 return _backwardrenames(x, y)
172 172 return _chain(x, y, _backwardrenames(x, a), _forwardcopies(a, y))
173 173
174 174 def mergecopies(repo, c1, c2, ca):
175 175 """
176 176 Find moves and copies between context c1 and c2 that are relevant
177 177 for merging.
178 178
179 179 Returns four dicts: "copy", "movewithdir", "diverge", and
180 180 "renamedelete".
181 181
182 182 "copy" is a mapping from destination name -> source name,
183 183 where source is in c1 and destination is in c2 or vice-versa.
184 184
185 185 "movewithdir" is a mapping from source name -> destination name,
186 186 where the file at source present in one context but not the other
187 187 needs to be moved to destination by the merge process, because the
188 188 other context moved the directory it is in.
189 189
190 190 "diverge" is a mapping of source name -> list of destination names
191 191 for divergent renames.
192 192
193 193 "renamedelete" is a mapping of source name -> list of destination
194 194 names for files deleted in c1 that were renamed in c2 or vice-versa.
195 195 """
196 196 # avoid silly behavior for update from empty dir
197 197 if not c1 or not c2 or c1 == c2:
198 198 return {}, {}, {}, {}
199 199
200 200 # avoid silly behavior for parent -> working dir
201 201 if c2.node() is None and c1.node() == repo.dirstate.p1():
202 202 return repo.dirstate.copies(), {}, {}, {}
203 203
204 204 limit = _findlimit(repo, c1.rev(), c2.rev())
205 205 if limit is None:
206 206 # no common ancestor, no copies
207 207 return {}, {}, {}, {}
208 208 m1 = c1.manifest()
209 209 m2 = c2.manifest()
210 210 ma = ca.manifest()
211 211
212 212 def makectx(f, n):
213 213 if len(n) != 20: # in a working context?
214 214 if c1.rev() is None:
215 215 return c1.filectx(f)
216 216 return c2.filectx(f)
217 217 return repo.filectx(f, fileid=n)
218 218
219 219 ctx = util.lrucachefunc(makectx)
220 220 copy = {}
221 221 movewithdir = {}
222 222 fullcopy = {}
223 223 diverge = {}
224 224
225 225 def related(f1, f2, limit):
226 226 # Walk back to common ancestor to see if the two files originate
227 227 # from the same file. Since workingfilectx's rev() is None it messes
228 228 # up the integer comparison logic, hence the pre-step check for
229 229 # None (f1 and f2 can only be workingfilectx's initially).
230 230
231 231 if f1 == f2:
232 232 return f1 # a match
233 233
234 234 g1, g2 = f1.ancestors(), f2.ancestors()
235 235 try:
236 236 f1r, f2r = f1.rev(), f2.rev()
237 237
238 238 if f1r is None:
239 239 f1 = g1.next()
240 240 if f2r is None:
241 241 f2 = g2.next()
242 242
243 243 while True:
244 244 f1r, f2r = f1.rev(), f2.rev()
245 245 if f1r > f2r:
246 246 f1 = g1.next()
247 247 elif f2r > f1r:
248 248 f2 = g2.next()
249 249 elif f1 == f2:
250 250 return f1 # a match
251 251 elif f1r == f2r or f1r < limit or f2r < limit:
252 252 return False # copy no longer relevant
253 253 except StopIteration:
254 254 return False
255 255
256 256 def checkcopies(f, m1, m2):
257 257 '''check possible copies of f from m1 to m2'''
258 258 of = None
259 259 seen = set([f])
260 260 for oc in ctx(f, m1[f]).ancestors():
261 261 ocr = oc.rev()
262 262 of = oc.path()
263 263 if of in seen:
264 264 # check limit late - grab last rename before
265 265 if ocr < limit:
266 266 break
267 267 continue
268 268 seen.add(of)
269 269
270 270 fullcopy[f] = of # remember for dir rename detection
271 271 if of not in m2:
272 272 continue # no match, keep looking
273 273 if m2[of] == ma.get(of):
274 274 break # no merge needed, quit early
275 275 c2 = ctx(of, m2[of])
276 276 cr = related(oc, c2, ca.rev())
277 277 if cr and (of == f or of == c2.path()): # non-divergent
278 278 copy[f] = of
279 279 of = None
280 280 break
281 281
282 282 if of in ma:
283 283 diverge.setdefault(of, []).append(f)
284 284
285 285 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
286 286
287 287 u1 = _nonoverlap(m1, m2, ma)
288 288 u2 = _nonoverlap(m2, m1, ma)
289 289
290 290 if u1:
291 291 repo.ui.debug(" unmatched files in local:\n %s\n"
292 292 % "\n ".join(u1))
293 293 if u2:
294 294 repo.ui.debug(" unmatched files in other:\n %s\n"
295 295 % "\n ".join(u2))
296 296
297 297 for f in u1:
298 298 checkcopies(f, m1, m2)
299 299 for f in u2:
300 300 checkcopies(f, m2, m1)
301 301
302 302 renamedelete = {}
303 303 renamedelete2 = set()
304 304 diverge2 = set()
305 305 for of, fl in diverge.items():
306 306 if len(fl) == 1 or of in c1 or of in c2:
307 307 del diverge[of] # not actually divergent, or not a rename
308 308 if of not in c1 and of not in c2:
309 309 # renamed on one side, deleted on the other side, but filter
310 310 # out files that have been renamed and then deleted
311 311 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
312 312 renamedelete2.update(fl) # reverse map for below
313 313 else:
314 314 diverge2.update(fl) # reverse map for below
315 315
316 316 if fullcopy:
317 317 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
318 318 "% = renamed and deleted):\n")
319 319 for f in sorted(fullcopy):
320 320 note = ""
321 321 if f in copy:
322 322 note += "*"
323 323 if f in diverge2:
324 324 note += "!"
325 325 if f in renamedelete2:
326 326 note += "%"
327 327 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
328 328 note))
329 329 del diverge2
330 330
331 331 if not fullcopy:
332 332 return copy, movewithdir, diverge, renamedelete
333 333
334 334 repo.ui.debug(" checking for directory renames\n")
335 335
336 336 # generate a directory move map
337 337 d1, d2 = c1.dirs(), c2.dirs()
338 d1.add('')
339 d2.add('')
338 d1.addpath('/')
339 d2.addpath('/')
340 340 invalid = set()
341 341 dirmove = {}
342 342
343 343 # examine each file copy for a potential directory move, which is
344 344 # when all the files in a directory are moved to a new directory
345 345 for dst, src in fullcopy.iteritems():
346 346 dsrc, ddst = _dirname(src), _dirname(dst)
347 347 if dsrc in invalid:
348 348 # already seen to be uninteresting
349 349 continue
350 350 elif dsrc in d1 and ddst in d1:
351 351 # directory wasn't entirely moved locally
352 352 invalid.add(dsrc)
353 353 elif dsrc in d2 and ddst in d2:
354 354 # directory wasn't entirely moved remotely
355 355 invalid.add(dsrc)
356 356 elif dsrc in dirmove and dirmove[dsrc] != ddst:
357 357 # files from the same directory moved to two different places
358 358 invalid.add(dsrc)
359 359 else:
360 360 # looks good so far
361 361 dirmove[dsrc + "/"] = ddst + "/"
362 362
363 363 for i in invalid:
364 364 if i in dirmove:
365 365 del dirmove[i]
366 366 del d1, d2, invalid
367 367
368 368 if not dirmove:
369 369 return copy, movewithdir, diverge, renamedelete
370 370
371 371 for d in dirmove:
372 372 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
373 373 (d, dirmove[d]))
374 374
375 375 # check unaccounted nonoverlapping files against directory moves
376 376 for f in u1 + u2:
377 377 if f not in fullcopy:
378 378 for d in dirmove:
379 379 if f.startswith(d):
380 380 # new file added in a directory that was moved, move it
381 381 df = dirmove[d] + f[len(d):]
382 382 if df not in copy:
383 383 movewithdir[f] = df
384 384 repo.ui.debug((" pending file src: '%s' -> "
385 385 "dst: '%s'\n") % (f, df))
386 386 break
387 387
388 388 return copy, movewithdir, diverge, renamedelete
@@ -1,832 +1,814 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 import errno
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import scmutil, util, ignore, osutil, parsers, encoding
12 12 import os, stat, errno, gc
13 13
14 14 propertycache = util.propertycache
15 15 filecache = scmutil.filecache
16 16 _rangemask = 0x7fffffff
17 17
18 18 class repocache(filecache):
19 19 """filecache for files in .hg/"""
20 20 def join(self, obj, fname):
21 21 return obj._opener.join(fname)
22 22
23 23 class rootcache(filecache):
24 24 """filecache for files in the repository root"""
25 25 def join(self, obj, fname):
26 26 return obj._join(fname)
27 27
28 def _incdirs(dirs, path):
29 for base in scmutil.finddirs(path):
30 if base in dirs:
31 dirs[base] += 1
32 return
33 dirs[base] = 1
34
35 def _decdirs(dirs, path):
36 for base in scmutil.finddirs(path):
37 if dirs[base] > 1:
38 dirs[base] -= 1
39 return
40 del dirs[base]
41
42 28 class dirstate(object):
43 29
44 30 def __init__(self, opener, ui, root, validate):
45 31 '''Create a new dirstate object.
46 32
47 33 opener is an open()-like callable that can be used to open the
48 34 dirstate file; root is the root of the directory tracked by
49 35 the dirstate.
50 36 '''
51 37 self._opener = opener
52 38 self._validate = validate
53 39 self._root = root
54 40 self._rootdir = os.path.join(root, '')
55 41 self._dirty = False
56 42 self._dirtypl = False
57 43 self._lastnormaltime = 0
58 44 self._ui = ui
59 45 self._filecache = {}
60 46
61 47 @propertycache
62 48 def _map(self):
63 49 '''Return the dirstate contents as a map from filename to
64 50 (state, mode, size, time).'''
65 51 self._read()
66 52 return self._map
67 53
68 54 @propertycache
69 55 def _copymap(self):
70 56 self._read()
71 57 return self._copymap
72 58
73 59 @propertycache
74 60 def _foldmap(self):
75 61 f = {}
76 62 for name in self._map:
77 63 f[util.normcase(name)] = name
78 64 for name in self._dirs:
79 65 f[util.normcase(name)] = name
80 66 f['.'] = '.' # prevents useless util.fspath() invocation
81 67 return f
82 68
83 69 @repocache('branch')
84 70 def _branch(self):
85 71 try:
86 72 return self._opener.read("branch").strip() or "default"
87 73 except IOError, inst:
88 74 if inst.errno != errno.ENOENT:
89 75 raise
90 76 return "default"
91 77
92 78 @propertycache
93 79 def _pl(self):
94 80 try:
95 81 fp = self._opener("dirstate")
96 82 st = fp.read(40)
97 83 fp.close()
98 84 l = len(st)
99 85 if l == 40:
100 86 return st[:20], st[20:40]
101 87 elif l > 0 and l < 40:
102 88 raise util.Abort(_('working directory state appears damaged!'))
103 89 except IOError, err:
104 90 if err.errno != errno.ENOENT:
105 91 raise
106 92 return [nullid, nullid]
107 93
108 94 @propertycache
109 95 def _dirs(self):
110 dirs = {}
111 for f, s in self._map.iteritems():
112 if s[0] != 'r':
113 _incdirs(dirs, f)
114 return dirs
96 return scmutil.dirs(self._map, 'r')
115 97
116 98 def dirs(self):
117 99 return self._dirs
118 100
119 101 @rootcache('.hgignore')
120 102 def _ignore(self):
121 103 files = [self._join('.hgignore')]
122 104 for name, path in self._ui.configitems("ui"):
123 105 if name == 'ignore' or name.startswith('ignore.'):
124 106 files.append(util.expandpath(path))
125 107 return ignore.ignore(self._root, files, self._ui.warn)
126 108
127 109 @propertycache
128 110 def _slash(self):
129 111 return self._ui.configbool('ui', 'slash') and os.sep != '/'
130 112
131 113 @propertycache
132 114 def _checklink(self):
133 115 return util.checklink(self._root)
134 116
135 117 @propertycache
136 118 def _checkexec(self):
137 119 return util.checkexec(self._root)
138 120
139 121 @propertycache
140 122 def _checkcase(self):
141 123 return not util.checkcase(self._join('.hg'))
142 124
143 125 def _join(self, f):
144 126 # much faster than os.path.join()
145 127 # it's safe because f is always a relative path
146 128 return self._rootdir + f
147 129
148 130 def flagfunc(self, buildfallback):
149 131 if self._checklink and self._checkexec:
150 132 def f(x):
151 133 try:
152 134 st = os.lstat(self._join(x))
153 135 if util.statislink(st):
154 136 return 'l'
155 137 if util.statisexec(st):
156 138 return 'x'
157 139 except OSError:
158 140 pass
159 141 return ''
160 142 return f
161 143
162 144 fallback = buildfallback()
163 145 if self._checklink:
164 146 def f(x):
165 147 if os.path.islink(self._join(x)):
166 148 return 'l'
167 149 if 'x' in fallback(x):
168 150 return 'x'
169 151 return ''
170 152 return f
171 153 if self._checkexec:
172 154 def f(x):
173 155 if 'l' in fallback(x):
174 156 return 'l'
175 157 if util.isexec(self._join(x)):
176 158 return 'x'
177 159 return ''
178 160 return f
179 161 else:
180 162 return fallback
181 163
182 164 def getcwd(self):
183 165 cwd = os.getcwd()
184 166 if cwd == self._root:
185 167 return ''
186 168 # self._root ends with a path separator if self._root is '/' or 'C:\'
187 169 rootsep = self._root
188 170 if not util.endswithsep(rootsep):
189 171 rootsep += os.sep
190 172 if cwd.startswith(rootsep):
191 173 return cwd[len(rootsep):]
192 174 else:
193 175 # we're outside the repo. return an absolute path.
194 176 return cwd
195 177
196 178 def pathto(self, f, cwd=None):
197 179 if cwd is None:
198 180 cwd = self.getcwd()
199 181 path = util.pathto(self._root, cwd, f)
200 182 if self._slash:
201 183 return util.normpath(path)
202 184 return path
203 185
204 186 def __getitem__(self, key):
205 187 '''Return the current state of key (a filename) in the dirstate.
206 188
207 189 States are:
208 190 n normal
209 191 m needs merging
210 192 r marked for removal
211 193 a marked for addition
212 194 ? not tracked
213 195 '''
214 196 return self._map.get(key, ("?",))[0]
215 197
216 198 def __contains__(self, key):
217 199 return key in self._map
218 200
219 201 def __iter__(self):
220 202 for x in sorted(self._map):
221 203 yield x
222 204
223 205 def iteritems(self):
224 206 return self._map.iteritems()
225 207
226 208 def parents(self):
227 209 return [self._validate(p) for p in self._pl]
228 210
229 211 def p1(self):
230 212 return self._validate(self._pl[0])
231 213
232 214 def p2(self):
233 215 return self._validate(self._pl[1])
234 216
235 217 def branch(self):
236 218 return encoding.tolocal(self._branch)
237 219
238 220 def setparents(self, p1, p2=nullid):
239 221 """Set dirstate parents to p1 and p2.
240 222
241 223 When moving from two parents to one, 'm' merged entries a
242 224 adjusted to normal and previous copy records discarded and
243 225 returned by the call.
244 226
245 227 See localrepo.setparents()
246 228 """
247 229 self._dirty = self._dirtypl = True
248 230 oldp2 = self._pl[1]
249 231 self._pl = p1, p2
250 232 copies = {}
251 233 if oldp2 != nullid and p2 == nullid:
252 234 # Discard 'm' markers when moving away from a merge state
253 235 for f, s in self._map.iteritems():
254 236 if s[0] == 'm':
255 237 if f in self._copymap:
256 238 copies[f] = self._copymap[f]
257 239 self.normallookup(f)
258 240 return copies
259 241
260 242 def setbranch(self, branch):
261 243 self._branch = encoding.fromlocal(branch)
262 244 f = self._opener('branch', 'w', atomictemp=True)
263 245 try:
264 246 f.write(self._branch + '\n')
265 247 f.close()
266 248
267 249 # make sure filecache has the correct stat info for _branch after
268 250 # replacing the underlying file
269 251 ce = self._filecache['_branch']
270 252 if ce:
271 253 ce.refresh()
272 254 except: # re-raises
273 255 f.discard()
274 256 raise
275 257
276 258 def _read(self):
277 259 self._map = {}
278 260 self._copymap = {}
279 261 try:
280 262 st = self._opener.read("dirstate")
281 263 except IOError, err:
282 264 if err.errno != errno.ENOENT:
283 265 raise
284 266 return
285 267 if not st:
286 268 return
287 269
288 270 # Python's garbage collector triggers a GC each time a certain number
289 271 # of container objects (the number being defined by
290 272 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
291 273 # for each file in the dirstate. The C version then immediately marks
292 274 # them as not to be tracked by the collector. However, this has no
293 275 # effect on when GCs are triggered, only on what objects the GC looks
294 276 # into. This means that O(number of files) GCs are unavoidable.
295 277 # Depending on when in the process's lifetime the dirstate is parsed,
296 278 # this can get very expensive. As a workaround, disable GC while
297 279 # parsing the dirstate.
298 280 gcenabled = gc.isenabled()
299 281 gc.disable()
300 282 try:
301 283 p = parsers.parse_dirstate(self._map, self._copymap, st)
302 284 finally:
303 285 if gcenabled:
304 286 gc.enable()
305 287 if not self._dirtypl:
306 288 self._pl = p
307 289
308 290 def invalidate(self):
309 291 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
310 292 "_ignore"):
311 293 if a in self.__dict__:
312 294 delattr(self, a)
313 295 self._lastnormaltime = 0
314 296 self._dirty = False
315 297
316 298 def copy(self, source, dest):
317 299 """Mark dest as a copy of source. Unmark dest if source is None."""
318 300 if source == dest:
319 301 return
320 302 self._dirty = True
321 303 if source is not None:
322 304 self._copymap[dest] = source
323 305 elif dest in self._copymap:
324 306 del self._copymap[dest]
325 307
326 308 def copied(self, file):
327 309 return self._copymap.get(file, None)
328 310
329 311 def copies(self):
330 312 return self._copymap
331 313
332 314 def _droppath(self, f):
333 315 if self[f] not in "?r" and "_dirs" in self.__dict__:
334 _decdirs(self._dirs, f)
316 self._dirs.delpath(f)
335 317
336 318 def _addpath(self, f, state, mode, size, mtime):
337 319 oldstate = self[f]
338 320 if state == 'a' or oldstate == 'r':
339 321 scmutil.checkfilename(f)
340 322 if f in self._dirs:
341 323 raise util.Abort(_('directory %r already in dirstate') % f)
342 324 # shadows
343 325 for d in scmutil.finddirs(f):
344 326 if d in self._dirs:
345 327 break
346 328 if d in self._map and self[d] != 'r':
347 329 raise util.Abort(
348 330 _('file %r in dirstate clashes with %r') % (d, f))
349 331 if oldstate in "?r" and "_dirs" in self.__dict__:
350 _incdirs(self._dirs, f)
332 self._dirs.addpath(f)
351 333 self._dirty = True
352 334 self._map[f] = (state, mode, size, mtime)
353 335
354 336 def normal(self, f):
355 337 '''Mark a file normal and clean.'''
356 338 s = os.lstat(self._join(f))
357 339 mtime = int(s.st_mtime)
358 340 self._addpath(f, 'n', s.st_mode,
359 341 s.st_size & _rangemask, mtime & _rangemask)
360 342 if f in self._copymap:
361 343 del self._copymap[f]
362 344 if mtime > self._lastnormaltime:
363 345 # Remember the most recent modification timeslot for status(),
364 346 # to make sure we won't miss future size-preserving file content
365 347 # modifications that happen within the same timeslot.
366 348 self._lastnormaltime = mtime
367 349
368 350 def normallookup(self, f):
369 351 '''Mark a file normal, but possibly dirty.'''
370 352 if self._pl[1] != nullid and f in self._map:
371 353 # if there is a merge going on and the file was either
372 354 # in state 'm' (-1) or coming from other parent (-2) before
373 355 # being removed, restore that state.
374 356 entry = self._map[f]
375 357 if entry[0] == 'r' and entry[2] in (-1, -2):
376 358 source = self._copymap.get(f)
377 359 if entry[2] == -1:
378 360 self.merge(f)
379 361 elif entry[2] == -2:
380 362 self.otherparent(f)
381 363 if source:
382 364 self.copy(source, f)
383 365 return
384 366 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
385 367 return
386 368 self._addpath(f, 'n', 0, -1, -1)
387 369 if f in self._copymap:
388 370 del self._copymap[f]
389 371
390 372 def otherparent(self, f):
391 373 '''Mark as coming from the other parent, always dirty.'''
392 374 if self._pl[1] == nullid:
393 375 raise util.Abort(_("setting %r to other parent "
394 376 "only allowed in merges") % f)
395 377 self._addpath(f, 'n', 0, -2, -1)
396 378 if f in self._copymap:
397 379 del self._copymap[f]
398 380
399 381 def add(self, f):
400 382 '''Mark a file added.'''
401 383 self._addpath(f, 'a', 0, -1, -1)
402 384 if f in self._copymap:
403 385 del self._copymap[f]
404 386
405 387 def remove(self, f):
406 388 '''Mark a file removed.'''
407 389 self._dirty = True
408 390 self._droppath(f)
409 391 size = 0
410 392 if self._pl[1] != nullid and f in self._map:
411 393 # backup the previous state
412 394 entry = self._map[f]
413 395 if entry[0] == 'm': # merge
414 396 size = -1
415 397 elif entry[0] == 'n' and entry[2] == -2: # other parent
416 398 size = -2
417 399 self._map[f] = ('r', 0, size, 0)
418 400 if size == 0 and f in self._copymap:
419 401 del self._copymap[f]
420 402
421 403 def merge(self, f):
422 404 '''Mark a file merged.'''
423 405 if self._pl[1] == nullid:
424 406 return self.normallookup(f)
425 407 s = os.lstat(self._join(f))
426 408 self._addpath(f, 'm', s.st_mode,
427 409 s.st_size & _rangemask, int(s.st_mtime) & _rangemask)
428 410 if f in self._copymap:
429 411 del self._copymap[f]
430 412
431 413 def drop(self, f):
432 414 '''Drop a file from the dirstate'''
433 415 if f in self._map:
434 416 self._dirty = True
435 417 self._droppath(f)
436 418 del self._map[f]
437 419
438 420 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
439 421 normed = util.normcase(path)
440 422 folded = self._foldmap.get(normed, None)
441 423 if folded is None:
442 424 if isknown:
443 425 folded = path
444 426 else:
445 427 if exists is None:
446 428 exists = os.path.lexists(os.path.join(self._root, path))
447 429 if not exists:
448 430 # Maybe a path component exists
449 431 if not ignoremissing and '/' in path:
450 432 d, f = path.rsplit('/', 1)
451 433 d = self._normalize(d, isknown, ignoremissing, None)
452 434 folded = d + "/" + f
453 435 else:
454 436 # No path components, preserve original case
455 437 folded = path
456 438 else:
457 439 # recursively normalize leading directory components
458 440 # against dirstate
459 441 if '/' in normed:
460 442 d, f = normed.rsplit('/', 1)
461 443 d = self._normalize(d, isknown, ignoremissing, True)
462 444 r = self._root + "/" + d
463 445 folded = d + "/" + util.fspath(f, r)
464 446 else:
465 447 folded = util.fspath(normed, self._root)
466 448 self._foldmap[normed] = folded
467 449
468 450 return folded
469 451
470 452 def normalize(self, path, isknown=False, ignoremissing=False):
471 453 '''
472 454 normalize the case of a pathname when on a casefolding filesystem
473 455
474 456 isknown specifies whether the filename came from walking the
475 457 disk, to avoid extra filesystem access.
476 458
477 459 If ignoremissing is True, missing path are returned
478 460 unchanged. Otherwise, we try harder to normalize possibly
479 461 existing path components.
480 462
481 463 The normalized case is determined based on the following precedence:
482 464
483 465 - version of name already stored in the dirstate
484 466 - version of name stored on disk
485 467 - version provided via command arguments
486 468 '''
487 469
488 470 if self._checkcase:
489 471 return self._normalize(path, isknown, ignoremissing)
490 472 return path
491 473
492 474 def clear(self):
493 475 self._map = {}
494 476 if "_dirs" in self.__dict__:
495 477 delattr(self, "_dirs")
496 478 self._copymap = {}
497 479 self._pl = [nullid, nullid]
498 480 self._lastnormaltime = 0
499 481 self._dirty = True
500 482
501 483 def rebuild(self, parent, allfiles, changedfiles=None):
502 484 changedfiles = changedfiles or allfiles
503 485 oldmap = self._map
504 486 self.clear()
505 487 for f in allfiles:
506 488 if f not in changedfiles:
507 489 self._map[f] = oldmap[f]
508 490 else:
509 491 if 'x' in allfiles.flags(f):
510 492 self._map[f] = ('n', 0777, -1, 0)
511 493 else:
512 494 self._map[f] = ('n', 0666, -1, 0)
513 495 self._pl = (parent, nullid)
514 496 self._dirty = True
515 497
516 498 def write(self):
517 499 if not self._dirty:
518 500 return
519 501 st = self._opener("dirstate", "w", atomictemp=True)
520 502
521 503 def finish(s):
522 504 st.write(s)
523 505 st.close()
524 506 self._lastnormaltime = 0
525 507 self._dirty = self._dirtypl = False
526 508
527 509 # use the modification time of the newly created temporary file as the
528 510 # filesystem's notion of 'now'
529 511 now = util.fstat(st).st_mtime
530 512 finish(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
531 513
532 514 def _dirignore(self, f):
533 515 if f == '.':
534 516 return False
535 517 if self._ignore(f):
536 518 return True
537 519 for p in scmutil.finddirs(f):
538 520 if self._ignore(p):
539 521 return True
540 522 return False
541 523
542 524 def walk(self, match, subrepos, unknown, ignored):
543 525 '''
544 526 Walk recursively through the directory tree, finding all files
545 527 matched by match.
546 528
547 529 Return a dict mapping filename to stat-like object (either
548 530 mercurial.osutil.stat instance or return value of os.stat()).
549 531 '''
550 532
551 533 def fwarn(f, msg):
552 534 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
553 535 return False
554 536
555 537 def badtype(mode):
556 538 kind = _('unknown')
557 539 if stat.S_ISCHR(mode):
558 540 kind = _('character device')
559 541 elif stat.S_ISBLK(mode):
560 542 kind = _('block device')
561 543 elif stat.S_ISFIFO(mode):
562 544 kind = _('fifo')
563 545 elif stat.S_ISSOCK(mode):
564 546 kind = _('socket')
565 547 elif stat.S_ISDIR(mode):
566 548 kind = _('directory')
567 549 return _('unsupported file type (type is %s)') % kind
568 550
569 551 ignore = self._ignore
570 552 dirignore = self._dirignore
571 553 if ignored:
572 554 ignore = util.never
573 555 dirignore = util.never
574 556 elif not unknown:
575 557 # if unknown and ignored are False, skip step 2
576 558 ignore = util.always
577 559 dirignore = util.always
578 560
579 561 matchfn = match.matchfn
580 562 matchalways = match.always()
581 563 badfn = match.bad
582 564 dmap = self._map
583 565 normpath = util.normpath
584 566 listdir = osutil.listdir
585 567 lstat = os.lstat
586 568 getkind = stat.S_IFMT
587 569 dirkind = stat.S_IFDIR
588 570 regkind = stat.S_IFREG
589 571 lnkkind = stat.S_IFLNK
590 572 join = self._join
591 573 work = []
592 574 wadd = work.append
593 575
594 576 exact = skipstep3 = False
595 577 if matchfn == match.exact: # match.exact
596 578 exact = True
597 579 dirignore = util.always # skip step 2
598 580 elif match.files() and not match.anypats(): # match.match, no patterns
599 581 skipstep3 = True
600 582
601 583 if not exact and self._checkcase:
602 584 normalize = self._normalize
603 585 skipstep3 = False
604 586 else:
605 587 normalize = None
606 588
607 589 files = sorted(match.files())
608 590 subrepos.sort()
609 591 i, j = 0, 0
610 592 while i < len(files) and j < len(subrepos):
611 593 subpath = subrepos[j] + "/"
612 594 if files[i] < subpath:
613 595 i += 1
614 596 continue
615 597 while i < len(files) and files[i].startswith(subpath):
616 598 del files[i]
617 599 j += 1
618 600
619 601 if not files or '.' in files:
620 602 files = ['']
621 603 results = dict.fromkeys(subrepos)
622 604 results['.hg'] = None
623 605
624 606 # step 1: find all explicit files
625 607 for ff in files:
626 608 if normalize:
627 609 nf = normalize(normpath(ff), False, True)
628 610 else:
629 611 nf = normpath(ff)
630 612 if nf in results:
631 613 continue
632 614
633 615 try:
634 616 st = lstat(join(nf))
635 617 kind = getkind(st.st_mode)
636 618 if kind == dirkind:
637 619 skipstep3 = False
638 620 if nf in dmap:
639 621 #file deleted on disk but still in dirstate
640 622 results[nf] = None
641 623 match.dir(nf)
642 624 if not dirignore(nf):
643 625 wadd(nf)
644 626 elif kind == regkind or kind == lnkkind:
645 627 results[nf] = st
646 628 else:
647 629 badfn(ff, badtype(kind))
648 630 if nf in dmap:
649 631 results[nf] = None
650 632 except OSError, inst:
651 633 if nf in dmap: # does it exactly match a file?
652 634 results[nf] = None
653 635 else: # does it match a directory?
654 636 prefix = nf + "/"
655 637 for fn in dmap:
656 638 if fn.startswith(prefix):
657 639 match.dir(nf)
658 640 skipstep3 = False
659 641 break
660 642 else:
661 643 badfn(ff, inst.strerror)
662 644
663 645 # step 2: visit subdirectories
664 646 while work:
665 647 nd = work.pop()
666 648 skip = None
667 649 if nd == '.':
668 650 nd = ''
669 651 else:
670 652 skip = '.hg'
671 653 try:
672 654 entries = listdir(join(nd), stat=True, skip=skip)
673 655 except OSError, inst:
674 656 if inst.errno in (errno.EACCES, errno.ENOENT):
675 657 fwarn(nd, inst.strerror)
676 658 continue
677 659 raise
678 660 for f, kind, st in entries:
679 661 if normalize:
680 662 nf = normalize(nd and (nd + "/" + f) or f, True, True)
681 663 else:
682 664 nf = nd and (nd + "/" + f) or f
683 665 if nf not in results:
684 666 if kind == dirkind:
685 667 if not ignore(nf):
686 668 match.dir(nf)
687 669 wadd(nf)
688 670 if nf in dmap and (matchalways or matchfn(nf)):
689 671 results[nf] = None
690 672 elif kind == regkind or kind == lnkkind:
691 673 if nf in dmap:
692 674 if matchalways or matchfn(nf):
693 675 results[nf] = st
694 676 elif (matchalways or matchfn(nf)) and not ignore(nf):
695 677 results[nf] = st
696 678 elif nf in dmap and (matchalways or matchfn(nf)):
697 679 results[nf] = None
698 680
699 681 for s in subrepos:
700 682 del results[s]
701 683 del results['.hg']
702 684
703 685 # step 3: report unseen items in the dmap hash
704 686 if not skipstep3 and not exact:
705 687 if not results and matchalways:
706 688 visit = dmap.keys()
707 689 else:
708 690 visit = [f for f in dmap if f not in results and matchfn(f)]
709 691 visit.sort()
710 692
711 693 if unknown:
712 694 # unknown == True means we walked the full directory tree above.
713 695 # So if a file is not seen it was either a) not matching matchfn
714 696 # b) ignored, c) missing, or d) under a symlink directory.
715 697 audit_path = scmutil.pathauditor(self._root)
716 698
717 699 for nf in iter(visit):
718 700 # Report ignored items in the dmap as long as they are not
719 701 # under a symlink directory.
720 702 if ignore(nf) and audit_path.check(nf):
721 703 try:
722 704 results[nf] = lstat(join(nf))
723 705 except OSError:
724 706 # file doesn't exist
725 707 results[nf] = None
726 708 else:
727 709 # It's either missing or under a symlink directory
728 710 results[nf] = None
729 711 else:
730 712 # We may not have walked the full directory tree above,
731 713 # so stat everything we missed.
732 714 nf = iter(visit).next
733 715 for st in util.statfiles([join(i) for i in visit]):
734 716 results[nf()] = st
735 717 return results
736 718
737 719 def status(self, match, subrepos, ignored, clean, unknown):
738 720 '''Determine the status of the working copy relative to the
739 721 dirstate and return a tuple of lists (unsure, modified, added,
740 722 removed, deleted, unknown, ignored, clean), where:
741 723
742 724 unsure:
743 725 files that might have been modified since the dirstate was
744 726 written, but need to be read to be sure (size is the same
745 727 but mtime differs)
746 728 modified:
747 729 files that have definitely been modified since the dirstate
748 730 was written (different size or mode)
749 731 added:
750 732 files that have been explicitly added with hg add
751 733 removed:
752 734 files that have been explicitly removed with hg remove
753 735 deleted:
754 736 files that have been deleted through other means ("missing")
755 737 unknown:
756 738 files not in the dirstate that are not ignored
757 739 ignored:
758 740 files not in the dirstate that are ignored
759 741 (by _dirignore())
760 742 clean:
761 743 files that have definitely not been modified since the
762 744 dirstate was written
763 745 '''
764 746 listignored, listclean, listunknown = ignored, clean, unknown
765 747 lookup, modified, added, unknown, ignored = [], [], [], [], []
766 748 removed, deleted, clean = [], [], []
767 749
768 750 dmap = self._map
769 751 ladd = lookup.append # aka "unsure"
770 752 madd = modified.append
771 753 aadd = added.append
772 754 uadd = unknown.append
773 755 iadd = ignored.append
774 756 radd = removed.append
775 757 dadd = deleted.append
776 758 cadd = clean.append
777 759 mexact = match.exact
778 760 dirignore = self._dirignore
779 761 checkexec = self._checkexec
780 762 checklink = self._checklink
781 763 copymap = self._copymap
782 764 lastnormaltime = self._lastnormaltime
783 765
784 766 lnkkind = stat.S_IFLNK
785 767
786 768 for fn, st in self.walk(match, subrepos, listunknown,
787 769 listignored).iteritems():
788 770 if fn not in dmap:
789 771 if (listignored or mexact(fn)) and dirignore(fn):
790 772 if listignored:
791 773 iadd(fn)
792 774 elif listunknown:
793 775 uadd(fn)
794 776 continue
795 777
796 778 state, mode, size, time = dmap[fn]
797 779
798 780 if not st and state in "nma":
799 781 dadd(fn)
800 782 elif state == 'n':
801 783 # The "mode & lnkkind != lnkkind or self._checklink"
802 784 # lines are an expansion of "islink => checklink"
803 785 # where islink means "is this a link?" and checklink
804 786 # means "can we check links?".
805 787 mtime = int(st.st_mtime)
806 788 if (size >= 0 and
807 789 ((size != st.st_size and size != st.st_size & _rangemask)
808 790 or ((mode ^ st.st_mode) & 0100 and checkexec))
809 791 and (mode & lnkkind != lnkkind or checklink)
810 792 or size == -2 # other parent
811 793 or fn in copymap):
812 794 madd(fn)
813 795 elif ((time != mtime and time != mtime & _rangemask)
814 796 and (mode & lnkkind != lnkkind or checklink)):
815 797 ladd(fn)
816 798 elif mtime == lastnormaltime:
817 799 # fn may have been changed in the same timeslot without
818 800 # changing its size. This can happen if we quickly do
819 801 # multiple commits in a single transaction.
820 802 # Force lookup, so we don't miss such a racy file change.
821 803 ladd(fn)
822 804 elif listclean:
823 805 cadd(fn)
824 806 elif state == 'm':
825 807 madd(fn)
826 808 elif state == 'a':
827 809 aadd(fn)
828 810 elif state == 'r':
829 811 radd(fn)
830 812
831 813 return (lookup, modified, added, removed, deleted, unknown, ignored,
832 814 clean)
General Comments 0
You need to be logged in to leave comments. Login now